SubNavigationPager is the root of a tabbed route. It renders a horizontal pager of pages with a SubNavigation strip overlaid at the top, keeps the active tab and the visible page in sync, and manages the focus handoff between the two.import { useState } from 'react';
import {
ListItem,
SubNavigationPager,
VerticalList,
} from '@wearables-ui-toolkit/mrbd';
export function ActivityRoute() {
const [pageIndex, setPageIndex] = useState(0);
return (
<SubNavigationPager
items={[
{ label: 'Recent', icon: clockIcon },
{ label: 'Saved', icon: bookmarkIcon },
]}
currentPageIndex={pageIndex}
onPageChange={setPageIndex}
ariaLabel="Activity"
>
<VerticalList insetForHeader ariaLabel="Recent activity">
<ListItem title="Morning brief" subtitle="Ready" onClick={openBrief} />
<ListItem title="Team notes" subtitle="Updated" onClick={openNotes} />
</VerticalList>
<VerticalList insetForHeader ariaLabel="Saved activity">
<ListItem title="Coastal trail" subtitle="Yesterday" onClick={openTrail} />
</VerticalList>
</SubNavigationPager>
);
}
onPageChange receives the new index, the previous index, and whether the move was animated, so a one-argument state setter is still a valid handler.VerticalList for each child because both destinations are lists. Other children can use any composition appropriate for their content. When a child has a scroller whose first content should clear the overlaid strip, set insetForHeader on that VerticalList or ScrollView; do not reproduce the clearance with padding.currentPageIndex off lets the component own the index, which is enough when nothing outside the route needs to read or restore it:<SubNavigationPager
items={items}
defaultPageIndex={1}
homeIndex={0}
ariaLabel="Library"
>
{pages}
</SubNavigationPager>
<SubNavigationPager
items={items}
currentPageIndex={pageIndex}
onPageChange={setPageIndex}
navigationLocked={isUploading}
onNavigationAttemptWhileLocked={showUploadInProgressToast}
ariaLabel="Library"
>
{pages}
</SubNavigationPager>
SubNavigationPagerProps. It is a closed interface: it does not spread arbitrary DOM attributes onto the root.| Prop | Type | Default | Description |
|---|---|---|---|
items | SubNavigationItem[] | — | One item per page, in display order. Required. |
children | ReactNode | — | Arbitrary page content, with one child per item. The counts must match. |
currentPageIndex | number | — | The visible page index (controlled). While it is provided the component never moves the page itself; it reports the request through onPageChange and waits for a new value. |
defaultPageIndex | number | 0 | The initially visible page. Used only when currentPageIndex is not provided. |
onPageChange | (newIndex: number, prevIndex: number, animated: boolean) => void | — | Called when the visible page changes, from a tab selection or from paging. Tab selections always report animated as true. |
useBackButtonForHome | boolean | true | Whether Back returns to the home page. Escape exercises the same behavior during desktop testing. |
homeIndex | number | -1 | Index of the home page. A negative value disables the Back-to-home behavior, so Back falls through. |
navigationLocked | boolean | false | Rejects requested page changes from the tabs, paging, and Back-to-home. Page-set reconciliation can still move the resolved index. |
onNavigationAttemptWhileLocked | () => void | — | Called each time a page change is attempted while locked. |
autoHide | boolean | false | Whether the strip fades out after a period without focus and fades back in on focus. |
subNavigationDisabled | boolean | false | Removes the strip from layout entirely. Independent of autoHide, which only animates opacity. |
enableTopPadding | boolean | true | Whether the strip carries its standard extra top margin. Turn it off only when the host already owns that inset. |
className | string | '' | Class added to the root alongside the component’s own class. |
style | CSSProperties | — | Inline styles applied to the root. |
ariaLabel | string | 'Navigation pager' | Accessible name for the tabbed region. |
div. It has no imperative handle.SubNavigationItem is documented on SubNavigation: a required label, a required icon, and an optional isLoading flag.null or false — what a {condition && <Saved />} expression yields when the condition fails — are not counted, so a conditionally omitted page has to drop its item too.Pager changes pages when Left or Right reaches a directional-focus boundary with no eligible target in the current page. That page change updates the active tab. This is directional paging, not pointer swiping or route navigation.currentPageIndex and the owner decides when the page moves. Omit it and the component tracks the index internally, starting at defaultPageIndex.navigationLocked, a tab selection fires onNavigationAttemptWhileLocked and nothing else — onPageChange is not called and the page does not move. The lock is forwarded to the pager, so paging from within page content is blocked the same way, and Back-to-home goes through the same path and is blocked too. Desktop Escape exercises that Back path during testing.ArrowDown from the strip moves focus to the first focusable element of the active page when one exists. Static pages are supported; without a focusable descendant, ArrowDown remains on the strip. Provide a meaningful focus target when users need to enter the page or control overflowing content with directional input.homeIndex when useBackButtonForHome is on, a non-negative home index is set, and the active page is not already home. Desktop Escape exercises the same behavior. The request is handled at the root, so it works from the strip and page content alike, and falls through in every other case. The pager does not store ordinary tab changes in route history.ariaLabel.ariaLabel so its content is identifiable beyond the page position.Page.insetForHeader on the VerticalList or ScrollView that should clear the overlaid strip. Static pages need no scroller.currentPageIndex in application or router state when a detail route can replace the pager. Back restores the originating tab only when the application restores that state; SubNavigationPager does not write tab changes to browser history.navigationLocked with onNavigationAttemptWhileLocked so a blocked tab press still explains itself.Page and SubNavigationPager when that would duplicate top chrome or create competing full-size layout owners.insetForHeader rather than authored top padding.ArrowDown remains on the strip.currentPageIndex state updated from onPageChange; otherwise, the tabs appear to do nothing.