Pager

Updated: Sep 24, 2026
Use Pager for discrete full-page or page-like destinations that users understand as a sequence. Each child becomes a page. Pager owns page positioning, transition animation, arrow-key navigation, initial-focus requests, and page lifecycle reporting so navigation remains consistent and accessible.
For items that users browse as a strip within one screen, prefer Carousel.
An uncontrolled pager owns its current index. Wrap content in PagerPage to declare page lifecycle callbacks. A plain child still becomes a page, but it has no lifecycle callbacks of its own.
import { Pager, PagerOrientation, PagerPage } from '@wearables-ui-toolkit/mrbd';

export function Onboarding({ startPreview }: { startPreview: () => void }) {
  return (
    <Pager
      aria-label="Onboarding"
      orientation={PagerOrientation.HORIZONTAL}
      requestInitialFocusOnMount
    >
      <PagerPage key="welcome">
        <WelcomeStep />
      </PagerPage>
      <PagerPage key="preview" onWillShowPage={startPreview}>
        <PreviewStep />
      </PagerPage>
      <PagerPage key="finish">
        <FinishStep />
      </PagerPage>
    </Pager>
  );
}
For a controlled pager, keep the index in application state and update it from onPageChange:
import { useState } from 'react';
import { Pager, PagerPage } from '@wearables-ui-toolkit/mrbd';

export function MediaPages({ items }: { items: ReadonlyArray<MediaItem> }) {
  const [pageIndex, setPageIndex] = useState(0);

  return (
    <Pager
      aria-label="Media"
      currentPageIndex={pageIndex}
      onPageChange={nextIndex => setPageIndex(nextIndex)}
    >
      {items.map(item => (
        <PagerPage key={item.id}>
          <MediaDetail item={item} />
        </PagerPage>
      ))}
    </Pager>
  );
}
When lifecycle behavior depends on state owned by the page, register it from inside the page rather than on PagerPage:
import { useMemo, useState } from 'react';
import { usePagerPageLifecycle } from '@wearables-ui-toolkit/mrbd';

function DraftPage({ saveDraft }: { saveDraft: () => void }) {
  const [isDirty, setIsDirty] = useState(false);

  usePagerPageLifecycle(
    useMemo(
      () => ({
        shouldPreventNavigation: () => isDirty,
        onWillHidePage: () => saveDraft(),
      }),
      [isDirty, saveDraft],
    ),
  );

  return <DraftEditor onChange={() => setIsDirty(true)} />;
}

Props

The complete prop type is PagerProps. It accepts standard div attributes except onChange, which is unsupported, and forwards them to the pager root. The component applies those attributes before its own attributes. As a result, the pager’s state attributes for orientation, current page, page count, transition, navigation lock, and peek take precedence over attributes with the same names from the caller.
PropTypeDefaultDescription
children
ReactNode
—
Each child becomes a page, whether or not it is a PagerPage.
orientation
PagerOrientation
PagerOrientation.HORIZONTAL
Sets the page travel axis and the arrow keys used for navigation.
currentPageIndex
number
—
Sets the visible page index and makes the pager controlled.
defaultPageIndex
number
0
Sets the initially visible page for an uncontrolled pager. Later changes are ignored.
animated
boolean
true
Controls animation for transitions the pager starts when the resolved index or page set changes. Arrow-key and back-to-home navigation always animate.
useBackButtonForHome
boolean
true
Controls whether Back returns to the home page. Escape exercises the same behavior during desktop testing.
homeIndex
number
-1
Sets the home page index. The default configures no home page, so Back falls through.
navigationLocked
boolean
false
Rejects user- and application-requested page changes while locked. Page-set reconciliation can still move the resolved index.
onNavigationAttemptWhileLocked
() => void
—
Called when navigation is attempted while locked. Page-level vetoes do not call it.
unmountInactivePages
boolean
false
Unmounts pages that are not visible after transitions settle.
requestInitialFocusOnMount
boolean
false
Sends a START initial-focus request to the first visible page after its content mounts.
onPageChange
(newIndex: number, prevIndex: number, animated: boolean) => void
—
Called when the current page changes. A controlled pager must update application state here.
className
string
''
Class added to the pager root.
style
CSSProperties
—
Inline styles applied to the pager root.
ariaLabel
string
—
Accessible-name alias. Prefer the standard aria-label attribute, which takes precedence when both are set.
tabIndex
number
0
Tab index of the pager root.
role
AriaRole
'region'
Role of the pager root.
aria-roledescription
string
'carousel'
Role description announced for the pager root.
onFocus
FocusEventHandler<HTMLDivElement>
—
Runs before the pager delegates focus into the current page. Call preventDefault to keep focus on the pager root.
onKeyDown
KeyboardEventHandler<HTMLDivElement>
—
Runs before the pager handles the key. Call preventDefault to suppress paging and back-to-home for that key.

PagerPage

All PagerPage props are optional. Every prop except children can also be registered from inside the page through usePagerPageLifecycle.
PropTypeDescription
children
ReactNode
Page content.
onWillShowPage
() => void
Called before the page becomes current. The call waits for content to mount when needed.
onWillHidePage
() => void
Called before the page stops being current.
onWillUnloadPage
() => void
Called before an inactive page is unmounted.
onRequestInitialFocus
(request: PagerInitialFocusRequest) => boolean or void
Called when the page becomes current and should choose initial focus. Return true when the page handles the request.
onWillInterceptBack
() => boolean
Called before Back-to-home handling. Return true to consume Back. Desktop Escape follows the same path.
shouldPreventNavigation
() => boolean
Return true to block navigation away from this page.
onAnimationCompleted
(request: PagerAnimationCompletedRequest) => void
Called after an animated transition into this page finishes.
peekBeforeNavigation
boolean
Controls whether the page runs the peek animation before navigating away.
A PagerPage must be a direct child of Pager for the component to read these props. A custom component that wraps PagerPage becomes an ordinary page without its own lifecycle. In that composition, register lifecycle behavior inside the custom component with usePagerPageLifecycle.
PagerPageLifecycle is PagerPageProps without children, and it is the argument type for usePagerPageLifecycle. Memoize the object passed to the hook to avoid re-registering it on every render. The hook throws when it runs outside the content of a pager page. Values registered through the hook take precedence over matching callbacks on PagerPage.

Types and enums

PagerOrientation
  • HORIZONTAL
  • VERTICAL
NavigationDirection describes the direction of the page transition. PagerInitialFocusRequest.direction reports this value.
  • LEFT — horizontal navigation to a higher index
  • RIGHT — horizontal navigation to a lower index
  • UP — vertical navigation to a higher index
  • DOWN — vertical navigation to a lower index
  • START — the first page shown when no previous page exists
  • BACK — back-to-home navigation
PagerInitialFocusRequest
  • direction: NavigationDirection
  • pageIndex: number
  • previousPageIndex: number — -1 when no previous page exists
PagerAnimationCompletedRequest
  • pageIndex: number
  • previousPageIndex: number — -1 when no previous page exists

Imperative handle

Pass a ref to access PagerHandle:
  • preloadPageIfNeeded(index: number): void — forces the page at index to mount off screen, including when unmountInactivePages is on, and cancels any earlier unloadPage call for the same index. The method ignores out-of-range indices. Consider it only when measured performance supports preloading because eagerly mounting a non-trivial page costs more than mounting it during navigation.
  • unloadPage(index: number): void — unmounts the page at index after firing onWillUnloadPage. It throws when the index is out of range or identifies the visible page. The page remains unmounted, even when unmountInactivePages is off, until it becomes current or is preloaded. Navigating back to it mounts it again.
  • isNavigationLocked(): boolean — reports whether navigation is currently locked.
  • pageCount(): number — reports the number of pages.
  • isPageTransitionInProgress(): boolean — reports whether a transition is animating.

Behavior

Index resolution. The pager truncates a requested index to an integer and clamps it to the available range. An out-of-range value therefore resolves to the nearest page. With no pages, the pager reports no current page. When the page set changes, the pager resolves the requested index against the updated set.
Controlled navigation. With currentPageIndex set, keyboard and focus-driven navigation call onPageChange and wait for the prop to change. Controlled index updates are forced navigation and bypass the current page’s shouldPreventNavigation because the application owns the index. If a page must veto an application-driven change, the application must gate its state update.
Keyboard. Horizontal pagers respond to ArrowLeft and ArrowRight. Vertical pagers respond to ArrowUp and ArrowDown. Directional focus moves within the current page first, and the page changes only when focus has reached the page edge in that direction. On the first or last page, the pager consumes the key rather than moving focus outside, as long as focus is within the current page. When a nested container exhausts its own directional focus targets, it passes the move to the pager, which changes pages. System Back returns to homeIndex when useBackButtonForHome is on and a home page is configured; Escape exercises the same behavior during desktop testing. The current page can consume that request first through onWillInterceptBack. The caller’s onKeyDown runs before desktop Escape handling and can call preventDefault to suppress it. A pager nested in an inactive page ignores keys so nested pagers do not compete for directional navigation.
Navigation lock. While navigationLocked is set, the pager rejects every navigation attempt and calls onNavigationAttemptWhileLocked. This includes an index that is out of range or already current and an index supplied through currentPageIndex. The pager applies a rejected controlled index after the lock is released. Reconciliation is the exception: when the page count changes and the resolved index must move because the current page no longer exists or a previously clamped index becomes available, the pager navigates without reporting a locked attempt.
Peek. With peekBeforeNavigation, the first navigation attempt in a given direction runs a short peek-and-restore animation instead of changing pages. A scrim appears at the edge where the next page would enter, and the next attempt in that direction navigates. Peek applies only to animated, input-driven navigation that the pager owns. Forced moves, including controlled index changes and reconciliation after page-set changes, skip it.
Lifecycle order. On a page change, the outgoing page receives onWillHidePage, followed by onWillShowPage for the incoming page. The incoming callback waits until its content mounts when necessary. After an animated transition settles, onAnimationCompleted fires once. onWillUnloadPage fires before a page is removed because unmountInactivePages is on or unloadPage was called.
Initial focus. When a page becomes current, the pager creates a PagerInitialFocusRequest and sends it to the page’s onRequestInitialFocus. If the callback does not return true, the pager focuses the first focusable element or, when none exists, the page container. The fallback considers only visible elements that are not clipped by the page or by an ancestor with clipped or scrollable overflow. It skips elements marked initialFocusEligible={false} and their descendants. It also skips a candidate that contains another candidate so focus reaches the innermost element. The pager sends the same request when its root receives focus and, when requestInitialFocusOnMount is set, on mount. Focus moves before the next paint, which prevents a frame from rendering with focus on the wrong element. Prefer this request over calling focus before the page mounts.
Mounting. The pager mounts all pages by default. With unmountInactivePages, it keeps the visible page mounted, along with the outgoing page during a transition, both pages involved in a peek, and pages mounted through preloadPageIfNeeded. Registered lifecycle behavior follows the child’s React key when the page set changes. Page content instead mounts by position: reordering pages remounts their content and discards local state. Prefer adding and removing pages at the end, and keep page order stable to preserve application state.
Sizing. Pages are absolutely positioned and fill the pager. The pager measures its own box, with the visual viewport as the fallback before the first measurement, to calculate transition distance. It measures again on resize. Prefer a responsive parent that the pager can fill over fixed dimensions.
Reversing mid-transition. The pager accepts new navigation while a transition is running. Rapid back-and-forth input therefore ends on the page the user most recently requested instead of entering a queue.
Reduced motion. The pager does not read motion preferences. animated controls transitions caused by resolved-index updates and page-set reconciliation. Directional paging and Back-to-home navigation still request animation. The current Pager API does not provide a prop that disables every input-driven transition; do not claim full reduced-motion support by setting animated={false} alone.

Accessibility

  • The root is a region with the role description of a carousel. It uses aria-label, or the ariaLabel alias, as its accessible name and otherwise defaults to a generic page-viewer name. Applications should supply a specific name so users can identify the sequence.
  • The caller can override role, aria-label, and aria-roledescription. The standard aria-label attribute takes precedence over the ariaLabel alias.
  • Each page is a group with the role description of a slide and a label that states its position in the sequence. The page is programmatically focusable so it can receive focus even when it contains no focusable controls.
  • Non-current pages are inert and aria-hidden, which keeps focus and assistive technology within the visible page.
  • The peek scrim is decorative and hidden from assistive technology.

Application responsibilities

  • Give every page a stable React key so lifecycle registration follows the intended page.
  • Keep application page state and any separate pagination indicator synchronized with the pager. Pager does not persist its index in browser history, and PageTransition does not capture pager position. If the current page must survive leaving and returning to a route, control currentPageIndex and store it outside the route component.
  • Prefer PagerPage for static lifecycle behavior and usePagerPageLifecycle for callbacks that depend on state or refs inside the page.
  • Let the pager place initial focus. Override placement through onRequestInitialFocus when the page has a more appropriate target.
  • Name the pager with aria-label so assistive technology announces a specific sequence rather than a generic page viewer.

Avoid this pattern

  • Avoid nesting a same-axis carousel or scroll region in a pager unless the directional interaction remains unambiguous.
  • Do not rely on shouldPreventNavigation to block a controlled index change; controlled navigation bypasses the page veto by contract.
  • When Back should return home, set homeIndex and keep useBackButtonForHome on. Verify both system Back on the device and Escape during desktop testing.
  • Consider preloadPageIfNeeded only after measuring a loading or transition issue rather than using it as a default performance measure.
  • Do not call unloadPage for the visible page because the method throws.