SubNavigationPager

Updated: Sep 24, 2026
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.
Prefer it as the primary surface for a tabbed route, in place of Page. Avoid nesting the two when that would create duplicate top chrome or competing full-size layout owners. Each child is one page and can render any React content.

Usage

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.
This example uses a 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.
Leaving 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>
While a long-running action is in flight, lock navigation and tell the user why the tabs are not responding:
<SubNavigationPager
  items={items}
  currentPageIndex={pageIndex}
  onPageChange={setPageIndex}
  navigationLocked={isUploading}
  onNavigationAttemptWhileLocked={showUploadInProgressToast}
  ariaLabel="Library"
>
  {pages}
</SubNavigationPager>

Props

The full prop type is SubNavigationPagerProps. It is a closed interface: it does not spread arbitrary DOM attributes onto the root.
PropTypeDefaultDescription
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.
The component forwards its ref to the root div. It has no imperative handle.

Types and enums

SubNavigationItem is documented on SubNavigation: a required label, a required icon, and an optional isLoading flag.

Behavior

  • One item per page. The number of children must equal the number of items. A mismatch throws at render, rather than silently dropping a tab or a page. Children that are 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.
  • Two-way sync. Choosing a tab shows the matching page. From page content, the underlying horizontal 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.
  • Controlled and uncontrolled. Pass currentPageIndex and the owner decides when the page moves. Omit it and the component tracks the index internally, starting at defaultPageIndex.
  • Locking. With 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.
  • Arrow keys. With the strip focused, the left and right arrow keys move between tabs and pages together, and focus stays on the strip so you can step through several pages in a row.
  • Focus handoff.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.
  • Inactive pages. Only the active page is exposed; the others are inert, so their controls cannot be focused or activated while off screen.
  • Back. System Back returns to 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.
  • Scrim. A gradient scrim fades in over the whole surface, under the strip, while the strip has focus: labels stay readable and the page recedes while you are choosing a tab. It fades back out as soon as focus moves into the page. It is decorative and passes pointer events through.
  • Layering. The layers, back to front, are the pager, the scrim, and the strip. The strip is centered at the top.
  • Sizing. The root fills the space it is given and clips its overflow. Page changes are horizontal.
  • Initial focus. The strip opts out of automatic initial focus, so opening a tabbed route never strands focus in the tab bar.

Accessibility

  • The root is a labelled group named by ariaLabel.
  • The strip supplies the tab semantics, the position announcement, and the arrow-key shortcuts; see SubNavigation.
  • Every page is a group announced as a slide and labelled with its position, and each page is its own focus section, so directional navigation resolves inside the current page first.
  • When a page has a scroller, give it a descriptive ariaLabel so its content is identifiable beyond the page position.
  • Because inactive pages are inert, assistive technology and keyboard focus only ever reach the visible page.
  • Prefer it as the primary surface for a tabbed route, in place of Page.
  • Choose each page’s React content for its task. If that content scrolls, give it one owner for the scrolling axis and set insetForHeader on the VerticalList or ScrollView that should clear the overlaid strip. Static pages need no scroller.
  • Prefer a one-word item label when it is clear; use a concise two-word label when needed.
  • Persist 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.
  • Provide a meaningful focus target when users need to move focus into a page or control overflowing content with directional input.
  • Pair navigationLocked with onNavigationAttemptWhileLocked so a blocked tab press still explains itself.

Avoid

  • Avoid nesting Page and SubNavigationPager when that would duplicate top chrome or create competing full-size layout owners.
  • Avoid adding an extra same-axis scroller or a fixed-height wrapper around page content.
  • When a scroller should clear the strip, use insetForHeader rather than authored top padding.
  • Static pages are valid. If users need to enter one from the tab strip, provide a meaningful focus target; otherwise, ArrowDown remains on the strip.
  • Keep route return outside the pager. The host or router owns system Back; the pager’s Back-to-home behavior only moves among its peer pages.
  • Keep controlled currentPageIndex state updated from onPageChange; otherwise, the tabs appear to do nothing.
  • Avoid rendering a page header alongside the strip.