App: var(--uit-color-background-window) with var(--uit-color-text-primary) for text. Do not replace it with a light or application-defined page background. On an additive-light display, unlit pixels form the background, which supports legibility without unnecessary light.Page fills the available space and clips its overflow. It owns two absolutely positioned sibling regions:| Region | Position | Notes |
|---|---|---|
Header | Top center, above content | Rendered whenever showHeader is true (the default), even with no header text. Its position never changes. |
Content | Fills from its top inset to the bottom edge | Inset below the system-bar region when enableSystemBarInset is true (the default). |
insetForHeader and a headerHeight measured through the page handle. Do not insert a spacer, because the scroller owns header clearance and focus reveal.import { useLayoutEffect, useRef, useState } from 'react';
import type { PageHandle } from '@wearables-ui-toolkit/mrbd';
const pageRef = useRef<PageHandle>(null);
const [headerHeight, setHeaderHeight] = useState(0);
useLayoutEffect(() => {
setHeaderHeight(pageRef.current?.getHeaderHeight() ?? 0);
}, []);
getHeaderHeight() returns 0 when the header is hidden, so the same code supports showHeader={false}.SubNavigationPager instead of Page. It is a full-bleed route root with its own top-centered tab bar. Do not nest it inside Page, because each component owns the route chrome and content region.| Region | Inline inset |
|---|---|
General page content you lay out yourself | var(--uit-spacing-large) |
ScrollView, VerticalList, ButtonRail | None — they reach both display edges |
Rows and buttons inside those | Supplied by the component ( VerticalList pads its row container, ButtonRail pads its button row) |
Content inside Container, StaticContainer, Surface, Panel | var(--uit-spacing-large) from the component edge, unless it is full bleed media |
A backdrop-style Panel | May extend edge to edge |
Modal | Stays inset |
VerticalList’s contentClassName or contentStyle. These props style the row container while preserving an edge-to-edge scroller.ListItem rows flush. The list component owns row spacing, so no additional gap is needed.ScrollView is the scrolling primitive. orientation defaults to vertical; width and height both default to filling the parent.VerticalList is the required host for a vertical sequence of ListItem rows. It combines ScrollView with a row container that applies the list inline inset.ButtonRail owns horizontal scrolling. A bottom rail can therefore coexist with a vertical list because each controls a different axis.fadingEdgeEnabled defaults to true, and each edge’s strength reflects the distance still hidden on that side, disappearing at the actual boundary. A design can override edge lengths with topFadingEdgeLength, bottomFadingEdgeLength, leftFadingEdgeLength, and rightFadingEdgeLength. Do not imitate these fades with gradient overlays or permanent clearance padding; both interfere with the scroller’s overflow signal.ButtonRail similarly draws fading edges at each end while buttons remain out of view.orientation. Control it with scrollbarEnabled or its alias showScrollbar. It is enabled by default but renders only when content overflows. Consider disabling it only for a short strip where it adds visual noise without useful position information.import { Button, ButtonRail, VerticalList } from '@wearables-ui-toolkit/mrbd';
<div className="route-layout">
<VerticalList className="route-scroller" ariaLabel="Messages">
{rows}
</VerticalList>
<ButtonRail className="route-actions">
<Button title="Reply" icon={replyIcon} onClick={onReply} />
<Button title="Mute" icon={muteIcon} onClick={onMute} />
</ButtonRail>
</div>;
.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);
}
minmax(0, 1fr) and min-block-size: 0 are required for the scroll row to shrink below its content height. Without either declaration, the grid can size the row to the entire list and push the action rail below the display. The xsmall inset below the rail is the only extra spacing added by the action region.Scrim with ScrimPosition.BOTTOM between the scroller and rail. Prefer this supported layer over a solid strip so the relationship between content and actions stays visible.Button, Chip, Tag, ListItem, and the page Header at intrinsic size. Avoid fixed width, fixed height, and track stretching for these components.headerMaxLines on Page and subtitleMaxLines on ListItem, rather than placing text in a fixed box.ButtonRail measures both its viewport and children, then recomputes its scroll offset when either changes. This keeps a button that expands on focus visible; a fixed-width wrapper prevents that calculation from reflecting the available space.Container, StaticContainer, Surface, Button, Chip, Tag, or the page Header inside another member of that set. Each component owns a material, corner radius, and content inset, and nesting duplicates all three.Panel as a panel rather than a list row. Prefer the toolkit component that directly represents an interaction instead of imitating one with rounded rectangles or combinations of other components.block-size: 100% instead of calculated heights.minmax(0, 1fr) and shrinkable children min-block-size: 0 or min-inline-size: 0. These declarations allow content to shrink below its natural size without pushing adjacent regions away.padding-inline, padding-block-end, and block-size so layout follows the writing mode.min(), max(), clamp(), a grid track, or a shadow as a hardcoded value. A different unit does not turn that value into a semantic token.import {
Button,
ButtonRail,
ListItem,
Page,
VerticalList,
type IconSource,
} from '@wearables-ui-toolkit/mrbd';
const replyIcon: IconSource = '/icons/reply.svg';
const readIcon: IconSource = '/icons/read.svg';
export interface Message {
id: string;
sender: string;
body: string;
time: string;
}
export interface ThreadRouteProps {
messages: ReadonlyArray<Message>;
onOpenMessage: (id: string) => void;
onReply: () => void;
onMarkRead: () => void;
}
export function ThreadRoute({
messages,
onOpenMessage,
onReply,
onMarkRead,
}: ThreadRouteProps) {
return (
<Page headerText="Field Ops" headerMetadata={`${messages.length} messages`}>
<div className="thread-layout">
<VerticalList className="thread-scroller" ariaLabel="Messages">
{messages.map(message => (
<ListItem
key={message.id}
title={message.sender}
subtitle={message.body}
timestamp={message.time}
onClick={() => onOpenMessage(message.id)}
/>
))}
</VerticalList>
<ButtonRail className="thread-actions">
<Button title="Reply" icon={replyIcon} onClick={onReply} />
<Button title="Mark read" icon={readIcon} onClick={onMarkRead} />
</ButtonRail>
</div>
</Page>
);
}
.thread-layout {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
block-size: 100%;
}
.thread-scroller {
min-block-size: 0;
}
.thread-actions {
padding-block-end: var(--uit-spacing-xsmall);
}
import {
Button,
ButtonGroup,
Page,
TextStyle,
TextView,
} from '@wearables-ui-toolkit/mrbd';
export interface ConfirmRouteProps {
onCancel: () => void;
onConfirm: () => void;
}
export function ConfirmRoute({ onCancel, onConfirm }: ConfirmRouteProps) {
return (
<Page headerText="Delete route">
<div className="confirm-layout">
<div className="confirm-body">
<TextView as="h2" textStyle={TextStyle.HEADING2}>
Delete this route?
</TextView>
<TextView textStyle={TextStyle.BODY2}>
Saved stops are removed from the device.
</TextView>
</div>
<ButtonGroup className="confirm-actions">
<Button title="Cancel" onClick={onCancel} />
<Button title="Delete" onClick={onConfirm} />
</ButtonGroup>
</div>
</Page>
);
}
.confirm-layout {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
block-size: 100%;
padding-inline: var(--uit-spacing-large);
}
.confirm-body {
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--uit-spacing-med-lg);
min-block-size: 0;
}
.confirm-actions {
padding-block-end: var(--uit-spacing-xsmall);
}
SubNavigationPager child can render any suitable React content. This example uses one VerticalList in each pane because every destination is a scrolling list. Sibling panes can each have a scroll scope because only the current pane participates in focus and scrolling.import {
ListItem,
SubNavigationPager,
VerticalList,
type SubNavigationItem,
} from '@wearables-ui-toolkit/mrbd';
const TABS: SubNavigationItem[] = [
{ label: 'Recent', icon: '/icons/recent.svg' },
{ label: 'People', icon: '/icons/people.svg' },
{ label: 'Alerts', icon: '/icons/alerts.svg' },
];
<SubNavigationPager items={TABS} homeIndex={0} ariaLabel="Home sections">
<VerticalList insetForHeader ariaLabel="Recent">{recentRows}</VerticalList>
<VerticalList insetForHeader ariaLabel="People">{peopleRows}</VerticalList>
<VerticalList insetForHeader ariaLabel="Alerts">
<ListItem title="Battery low" subtitle="Charge soon" />
</VerticalList>
</SubNavigationPager>;
insetForHeader on that VerticalList or ScrollView rather than adding a spacer or padded wrapper.