App at the root and a clear primary surface for each route. When content scrolls, give the active route or pager page one owner for that axis; static pages need no scroller. These ownership boundaries keep focus and scrolling predictable.| Shape | Route root(s) | Owns the content | Owns the action |
|---|---|---|---|
Routed list | Page per route | VerticalList of ListItem, then ScrollView + Panel | ButtonRail on the detail route |
Tabbed control surface | one SubNavigationPager | two list-based control pages in this example | integrated ListItem controls |
Media browser | Page per route | Carousel of Card, then StaticContainer + Panel | the Card itself |
Status surface | one Page | ScrollView + Panel | one persistent Button in a ButtonRail |
Timed session | one Page | ScrollView + Panel | one phase-derived Button |
Work queue with row commands | one Page | VerticalList of ListItem | anchored VerticalMenu |
Multi-area workspace | one SubNavigationPager | content chosen for each area | actions scoped to an area |
/react-router subpath, not the root package.import { App } from '@wearables-ui-toolkit/mrbd';
import {
ReactRouterNavigationProvider,
ReactRouterPageTransition,
} from '@wearables-ui-toolkit/mrbd/react-router';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
function Root() {
return (
<BrowserRouter>
<ReactRouterNavigationProvider>
<App>
<RecordProvider>
<ReactRouterPageTransition>
{({ location }) => (
<Routes location={location}>
<Route path="/" element={<CollectionPage />} />
<Route path="/item/:id" element={<DetailPage />} />
</Routes>
)}
</ReactRouterPageTransition>
</RecordProvider>
</App>
</ReactRouterNavigationProvider>
</BrowserRouter>
);
}
ReactRouterPageTransition so route changes preserve application state. ReactRouterPageTransition supplies its transition state through a render callback; pass its location to Routes.Page > VerticalList > ListItem for the collection route. This direct composition gives the list one scroll and focus owner, without an additional ScrollView, summary, section heading, Panel, footer, gap, divider, or action rail.import { ListItem, Page, VerticalList } from '@wearables-ui-toolkit/mrbd';
<Page headerText="Items">
<VerticalList insetForHeader ariaLabel="Items">
{records.map(record => (
<ListItem
key={record.id}
title={record.title}
subtitle={record.subtitle}
timestamp={formatPointInTime(record.timestamp)}
onClick={() => navigate(`/item/${record.id}`)}
/>
))}
</VerticalList>
</Page>
ScrollView, one edge-to-edge Panel as its direct child, and a ButtonRail as the shell’s second track.Page header props for route identity instead of a standalone Header.VerticalList with ListItem for the collection.ScrollView with tabIndex={0} and a concise ariaLabel, so directional input can scroll without a child focus target.Panel with width="100%" as the detail information backdrop.TextView, TextStyle, and TextColor.Button within a ButtonRail, then use Toast to confirm its result.PageTransition restores the focused row and scroll offset.Panel to a panel per field, a second panel, or a field grid.SubNavigationPager does not require a fixed number or type of pages: every navigation item maps to one React child, and that child can render any content appropriate for the destination. Prefer it to Page as the primary surface for a tabbed route; keep rails and groups as action surfaces rather than using them as tabs.import { ListItem, SubNavigationPager, VerticalList } from '@wearables-ui-toolkit/mrbd';
<SubNavigationPager
items={[
{ label: 'Controls', icon: '/icons/sliders.svg' },
{ label: 'Modes', icon: '/icons/modes.svg' },
]}
currentPageIndex={pageIndex}
onPageChange={setPageIndex}
homeIndex={0}
ariaLabel="Device controls"
>
<VerticalList insetForHeader ariaLabel="Controls">
{controls.map(control => (
<ListItem
key={control.id}
title={control.title}
subtitle={control.subtitle}
showSwitch
checked={control.enabled}
onCheckedChange={checked => setControl(control.id, checked)}
/>
))}
</VerticalList>
<VerticalList insetForHeader ariaLabel="Modes">
{modes.map(mode => (
<ListItem
key={mode.id}
title={mode.title}
subtitle={mode.subtitle}
showRadioButton
checked={mode.id === activeModeId}
onCheckedChange={checked => {
if (checked) {
setActiveMode(mode.id);
}
}}
/>
))}
</VerticalList>
</SubNavigationPager>
SubNavigationItem its required icon.VerticalList because its pages are lists of settings.insetForHeader on the VerticalList or ScrollView that should clear the overlaid tab strip; static pages need neither a scroller nor this inset.showSwitch or showRadioButton. A standalone Switch or RadioButton beside a separately focusable label creates two targets for one setting.false callback must not clear the current selection because the control requires a valid value.Toast for outcomes that need transient confirmation rather than every toggle.Page > one edge-to-edge ScrollView > one direct Carousel > interactive Card destinations.Page > one ScrollView with tabIndex={0} > one non-interactive StaticContainer media region > one edge-to-edge Panel.Card with bottomScrim or topScrim, CardBelowScrim for the image layer, and CardAboveScrim for compact copy.Carousel with initialIndex from durable state and persist changes through onItemCentered. Avoid restoring position with a timeout or effect.StaticContainer with clipContent for detail media. Card is always focusable and represents a destination, so it is not a decorative hero.Panel with width="100%".Card geometry through its public props: a responsive width bounded by both axes and either an aspect-ratio or explicit height. Positioned layer wrappers cannot establish card geometry.Card must have an onClick and a complete ariaLabel because it is a focusable destination.Carousel as a direct ScrollView child. A padded wrapper breaks its edge-to-edge frame and pagination placement.Card, StaticContainer, and Panel through public root props rather than implementation details.import { useState } from 'react';
import {
Button,
ButtonRail,
Page,
Panel,
ScrollView,
TextColor,
TextStyle,
TextView,
Toast,
} from '@wearables-ui-toolkit/mrbd';
export function StatusPage() {
const [acknowledged, setAcknowledged] = useState(false);
const handleToggle = () => {
setAcknowledged(current => !current);
Toast.show(acknowledged ? 'Reopened' : 'Acknowledged');
};
return (
<Page headerText="Cold store">
<div className="action-page-shell">
<ScrollView insetForHeader tabIndex={0} ariaLabel="Cold store status">
<Panel width="100%">
<div className="content-inset">
<TextView as="p" textStyle={TextStyle.BODY2_EMPHASIZED}>
{acknowledged ? 'Door event acknowledged' : 'Door event recorded'}
</TextView>
<TextView as="p" textStyle={TextStyle.BODY2}>
Temperature monitoring continues without interruption.
</TextView>
<TextView
as="p"
textStyle={TextStyle.LABEL}
textColor={TextColor.SECONDARY}
>
STATUS
</TextView>
<TextView as="p" textStyle={TextStyle.META1}>
{acknowledged ? 'Acknowledged' : 'Needs review'}
</TextView>
</div>
</Panel>
</ScrollView>
<div className="action-dock">
<ButtonRail>
<Button
title={acknowledged ? 'Reopen' : 'Acknowledge'}
onClick={handleToggle}
/>
</ButtonRail>
</div>
</div>
</Page>
);
}
Button mounted across state changes so focus retains its owner. Derive its title, the status value, the summary, and the toast from the same state.Panel to a small, fixed set of text roles: a BODY2_EMPHASIZED summary, one BODY2 supporting sentence, an all-caps LABEL eyebrow with TextColor.SECONDARY, and the current value in META1. Prefer body, label, and metadata styles for routine content.type SessionPhase = 'running' | 'paused' | 'complete';
phase state and one non-negative remaining-time state. One effect that depends only on phase should own the running interval and transition to complete at zero.phase. A running label or pause action must not remain at zero because it would expose an invalid command.Button mounted in every phase so completion preserves focus and replaces obsolete commands in place.ProgressIndicator, ProgressRing, or CircularProgressBar for a determinate value, and IndeterminateLoader when completion time is unknown. Prefer these accessible components to authored progress markup.import { useRef, useState } from 'react';
import {
ListItem,
Page,
Toast,
TooltipMode,
VerticalList,
VerticalMenu,
VerticalMenuButton,
VerticalMenuCorner,
getVerticalMenuAnchorProps,
} from '@wearables-ui-toolkit/mrbd';
export function QueuePage() {
const [openRecordId, setOpenRecordId] = useState<string | null>(null);
const rowRefs = useRef(new Map<string, HTMLDivElement>());
const closeAndRestoreFocus = (id: string) => {
setOpenRecordId(null);
requestAnimationFrame(() => {
rowRefs.current.get(id)?.focus({ preventScroll: true });
});
};
return (
<Page headerText="Queue">
<VerticalList insetForHeader ariaLabel="Queue">
{records.map(record => (
<ListItem
key={record.id}
ref={node => {
if (node == null) {
rowRefs.current.delete(record.id);
} else {
rowRefs.current.set(record.id, node);
}
}}
title={record.title}
subtitle={record.subtitle}
ariaLabel="Open actions"
onClick={() => setOpenRecordId(record.id)}
tooltipMode={
openRecordId === record.id ? TooltipMode.ALWAYS : TooltipMode.NONE
}
tooltipContent={
<VerticalMenu
autoFocusFirstItem
onDismissRequest={() => closeAndRestoreFocus(record.id)}
>
<VerticalMenuButton
text="Assign"
onClick={event => {
event.stopPropagation();
assign(record.id);
closeAndRestoreFocus(record.id);
Toast.show('Assigned');
}}
/>
<VerticalMenuButton
text="Snooze"
onClick={event => {
event.stopPropagation();
snooze(record.id);
closeAndRestoreFocus(record.id);
Toast.show('Snoozed');
}}
/>
</VerticalMenu>
}
{...getVerticalMenuAnchorProps(VerticalMenuCorner.ABOVE_RIGHT)}
/>
))}
</VerticalList>
</Page>
);
}
VerticalMenuButton labels with text, not title, and keep them to one line.Toast.onDismissRequest. VerticalMenuDismissReason reports whether Escape or navigation dismissed the menu.ContextMenu with ButtonContextMenuItemView or EmojiContextMenuItemView for horizontal, icon-led commands. Its dismiss callback is onDismiss(reason), with reasons from DismissReason.SwipeToReveal when every row has the same one to three icon-only commands. It wraps one focusable child and reveals an ordered actions tray, with each action shaped as { icon, contentDescription, onClick }; the shell is not another focus stop.SubNavigationPager: a compact overview with bottom actions, an actionable list, and a read-only surface. Other pager pages can use any content and composition appropriate for their area.SubNavigationPager as the primary route surface and provide one child for each navigation item. If a child scrolls, give it one vertical scroll owner and apply insetForHeader to that owner; a static child needs neither.ButtonRail in the auto track. A rail beside the tab bar would mix area navigation and local actions.import { Shimmer, ShimmerItem, ShimmerItemCornerRadius } from '@wearables-ui-toolkit/mrbd';
<Shimmer showShimmer={isLoading}>
<ShimmerItem
width="100%"
cornerRadius={ShimmerItemCornerRadius.MEDIUM}
className="row-skeleton"
/>
</Shimmer>
Shimmer with ShimmerItem when the pending structure is known, and IndeterminateLoader when completion time is unknown.ButtonHandle.animateActionTransition({ endingIcon, endingTitle }) supports this send-then-sent case.Page or pager child, render an empty state as ordinary inset ScrollView content: a short BODY2_EMPHASIZED line, one BODY2 sentence, and one Button only when a real recovery path exists. Set tabIndex={0} and an ariaLabel so the scroller remains operable.ListItem; a fabricated row creates a false focus target. Likewise, avoid inventing an add or import action the application does not support.Modal for structured education content on a dedicated route when the task must pause. Choose its icon, bannerSrc, logoSrc, avatarSrc, or listItems slot and place a ButtonGroup in buttons. Add a leading visual only when it explains the state. Modal does not provide overlay or dismissal behavior itself.Modal for a blocking requirement. Use separate presentation infrastructure only when it explicitly owns the backdrop, focus containment, and dismissal behavior that Modal does not provide.Toast for non-blocking feedback.ListItem for actionable settings rows.ButtonGroup or a docked ButtonRail.Modal does not enforce that boundary; a dedicated route avoids exposing background controls.disabled state. Do not leave a focusable callback that returns without feedback.