| Layer | Rendered | Owns |
|---|---|---|
Shell | once, at the entry point | App, the router, the page transition |
Route surface | normally one primary surface per route | Page or SubNavigationPager |
Regions | inside the route surface | task-appropriate content and an optional docked action row; one owner per scrolling axis when needed |
App fills its parent. Use the semantic window-background role for the document instead of a literal color so the additive-light display leaves background pixels unlit.html,
body,
#root {
block-size: 100%;
background-color: var(--uit-color-background-window);
}
App once around the entire application. It installs the font faces, focus-navigation context with automatic initial focus, portal root for floating overlays, toast host, and theme scope that provides the window background and text color. These services work together, so do not build a partial shell that leaves overlays, toasts, or directional navigation without their required context.import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { App } from '@wearables-ui-toolkit/mrbd';
import {
ReactRouterNavigationProvider,
ReactRouterPageTransition,
} from '@wearables-ui-toolkit/mrbd/react-router';
import { InboxRoute } from './routes/InboxRoute';
import { LibraryRoute } from './routes/LibraryRoute';
import { SettingsRoute } from './routes/SettingsRoute';
import { ThreadRoute } from './routes/ThreadRoute';
function AppRoutes() {
return (
<ReactRouterNavigationProvider fallbackTo="/">
<ReactRouterPageTransition>
{({ location }) => (
<Routes location={location}>
<Route path="/" element={<InboxRoute />} />
<Route path="/thread/:threadId" element={<ThreadRoute />} />
<Route path="/library" element={<LibraryRoute />} />
<Route path="/settings" element={<SettingsRoute />} />
</Routes>
)}
</ReactRouterPageTransition>
</ReactRouterNavigationProvider>
);
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App>
<AppRoutes />
</App>
</BrowserRouter>
</StrictMode>,
);
ReactRouterNavigationProvider registers the application-level Back handler. It pops history and, at the first history entry, navigates to fallbackTo ('/' by default). It also owns route preloading, so provide preloadRoute here when destinations are preloaded, and holds the focused control’s visual state across route changes.ReactRouterPageTransition supplies a render function with the location snapshot currently being animated. Pass that snapshot to Routes; otherwise, Back can render the first matching route instead of the retained outgoing page.ReactRouterPageTransition forwards every PageTransition prop except direction and transitionKey, which it derives from history, and children, which also accepts the render function shown above. The most commonly configured props are config (DefaultPageTransitionConfig, InAppPageTransitionConfig, or AppSwitchPageTransitionConfig), initialFocus ('top-left' by default), and preservePageState (true by default).NextNavigationProvider and NextPageTransition from @wearables-ui-toolkit/mrbd/next. Because Next.js resolves a layout’s children from the current route, NextPageTransition renders each screen from the pathname it supplies. Any other router composes PageTransition, useBackNavigation, RoutePreloadProvider, and useNavigationFocusRetention directly. Route navigation shows both shells.getTransitionKey defines when a location represents a different page. It defaults to the location pathname, so changing only the search string or hash re-renders in place without an animation. getHistoryIndex changes how the adapter reads history depth to choose a 'forward' or 'back' transition. Prefer the default unless the host maintains its own history stack.Page routePage for a single-screen route. It pins a header at the top center and creates a content layer from the same top inset to the bottom edge. The header floats above that layer, allowing scrolling content to pass underneath it.showHeader defaults to true and is the only setting that hides the header; an empty header string still renders the header region. enableSystemBarInset defaults to true and places the content layer below the display’s system-bar region without moving the header.import { Page, TextStyle, TextView } from '@wearables-ui-toolkit/mrbd';
export function SettingsRoute() {
return (
<Page headerText="Settings" headerMetadata="3 items">
<div className="route-body">
<TextView as="h2" textStyle={TextStyle.HEADING2}>
Display
</TextView>
<TextView textStyle={TextStyle.BODY2}>
Brightness adapts to ambient light.
</TextView>
</div>
</Page>
);
}
.route-body {
display: flex;
flex-direction: column;
gap: var(--uit-spacing-med-lg);
block-size: 100%;
padding-inline: var(--uit-spacing-large);
}
large inline inset to general content that is not a scroller, list, or rail. Keep scrollers, lists, and rails edge to edge because their children provide the appropriate inset.ListItem rows in VerticalList. This is the required list host: it is a ScrollView, and its row container already applies the list inline inset. Render it directly in the page content without wrapper padding, and keep adjacent rows flush without an extra gap.import { ListItem, Page, VerticalList } from '@wearables-ui-toolkit/mrbd';
import { useNavigate } from 'react-router-dom';
export function InboxRoute() {
const navigate = useNavigate();
return (
<Page headerText="Inbox" headerMetadata="2 new">
<VerticalList ariaLabel="Inbox threads">
<ListItem
title="Sam Rivera"
subtitle="On my way"
timestamp="9:41"
onClick={() => navigate('/thread/sam')}
/>
<ListItem
title="Field Ops"
subtitle="Route updated"
timestamp="9:12"
onClick={() => navigate('/thread/ops')}
/>
</VerticalList>
</Page>
);
}
insetForHeader and pass the header height measured through the page handle.import { useLayoutEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
ListItem,
Page,
VerticalList,
type PageHandle,
} from '@wearables-ui-toolkit/mrbd';
export function LibraryRoute() {
const navigate = useNavigate();
const pageRef = useRef<PageHandle>(null);
const [headerHeight, setHeaderHeight] = useState(0);
useLayoutEffect(() => {
setHeaderHeight(pageRef.current?.getHeaderHeight() ?? 0);
}, []);
return (
<Page ref={pageRef} headerText="Library">
<VerticalList insetForHeader headerHeight={headerHeight}>
<ListItem
title="Recent"
subtitle="12 items"
onClick={() => navigate('/library/recent')}
/>
<ListItem
title="Saved"
subtitle="4 items"
onClick={() => navigate('/library/saved')}
/>
</VerticalList>
</Page>
);
}
getHeaderHeight() returns 0 while the header is hidden, so this approach also works with showHeader={false}.ButtonRail owns horizontal scrolling and its overflow fades. Because it scrolls on a different axis, it can coexist with the route’s vertical scroller.import { useParams } from 'react-router-dom';
import {
Button,
ButtonRail,
ListItem,
Page,
VerticalList,
type IconSource,
} from '@wearables-ui-toolkit/mrbd';
import { archiveThread, shareThread, useThread } from '../data/threads';
const replyIcon: IconSource = '/icons/reply.svg';
const archiveIcon: IconSource = '/icons/archive.svg';
const shareIcon: IconSource = '/icons/share.svg';
export function ThreadRoute() {
const { threadId = '' } = useParams();
const thread = useThread(threadId);
return (
<Page headerText={thread.title} headerMetadata={thread.status}>
<div className="route-layout">
<VerticalList className="route-scroller" ariaLabel="Messages">
{thread.messages.map(message => (
<ListItem
key={message.id}
title={message.sender}
subtitle={message.body}
timestamp={message.time}
/>
))}
</VerticalList>
<ButtonRail className="route-actions">
<Button title="Reply" icon={replyIcon} onClick={thread.reply} />
<Button
title="Archive"
icon={archiveIcon}
onClick={() => archiveThread(threadId)}
/>
<Button
title="Share"
icon={shareIcon}
onClick={() => shareThread(threadId)}
/>
</ButtonRail>
</div>
</Page>
);
}
.route-layout {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
block-size: 100%;
min-block-size: 0;
min-inline-size: 0;
}
.route-scroller {
min-block-size: 0;
min-inline-size: 0;
}
.route-actions {
min-inline-size: 0;
padding-block-end: var(--uit-spacing-xsmall);
}
block-size: 100% uses the height already calculated by the page content layer, so the route does not need the display dimensions.minmax(0, 1fr) on the scroll row and min-block-size: 0 on the scroller let that row shrink below its content height. Both are required; without them, the full list height can push the rail below the display.auto preserves the action row’s intrinsic height as its buttons resolve.xsmall bottom inset is the action region’s only additional spacing.SubNavigationPager as the primary surface for a compact set of peer destinations. It is full bleed, draws its own tab bar at the top center, and fades in a scrim behind the focused tab bar. Avoid placing it inside Page when that would create duplicate top chrome or competing full-size layout owners.items entry must have one corresponding child; a mismatch throws during render. A child can render any React content. Wrap it with PagerPage only when that pane needs lifecycle callbacks.import {
ListItem,
PagerPage,
SubNavigationPager,
VerticalList,
type IconSource,
type SubNavigationItem,
} from '@wearables-ui-toolkit/mrbd';
import { markAlertsSeen } from '../data/alerts';
const homeIcon: IconSource = '/icons/home.svg';
const peopleIcon: IconSource = '/icons/people.svg';
const alertsIcon: IconSource = '/icons/alerts.svg';
const TABS: SubNavigationItem[] = [
{ label: 'Home', icon: homeIcon },
{ label: 'People', icon: peopleIcon },
{ label: 'Alerts', icon: alertsIcon },
];
export function HomeRoute() {
return (
<SubNavigationPager
items={TABS}
defaultPageIndex={0}
homeIndex={0}
ariaLabel="Home sections"
>
<VerticalList ariaLabel="Home">
<ListItem title="Next stop" subtitle="Warehouse 4" />
</VerticalList>
<VerticalList ariaLabel="People">
<ListItem title="Sam Rivera" subtitle="Available" />
</VerticalList>
<PagerPage onWillShowPage={markAlertsSeen}>
<VerticalList ariaLabel="Alerts">
<ListItem title="Battery low" subtitle="Charge soon" />
</VerticalList>
</PagerPage>
</SubNavigationPager>
);
}
homeIndex to make Back return to the home tab before leaving the route. useBackButtonForHome defaults to true and must remain enabled for that behavior. Navigation and Focus explains focus movement between the tab bar and current pane.defaultPageIndex creates an uncontrolled pager, which is typically appropriate when the route always opens on the same tab. To preserve the selected tab after leaving and returning, control the pager with currentPageIndex and onPageChange, and store that index outside the route so it survives the route lifecycle.min(), max(), clamp(), or a grid track, so routes respond to their container.Container, StaticContainer, Surface, Button, Chip, Tag, or the page Header inside another member of that set; nesting duplicates material, corner radius, and inset.