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.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>
);
}
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>
);
}
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)} />;
}
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.| Prop | Type | Default | Description |
|---|---|---|---|
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. |
PagerPagePagerPage props are optional. Every prop except children can also be registered from inside the page through usePagerPageLifecycle.| Prop | Type | Description |
|---|---|---|
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. |
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.PagerOrientationHORIZONTALVERTICALNavigationDirection describes the direction of the page transition. PagerInitialFocusRequest.direction reports this value.LEFT — horizontal navigation to a higher indexRIGHT — horizontal navigation to a lower indexUP — vertical navigation to a higher indexDOWN — vertical navigation to a lower indexSTART — the first page shown when no previous page existsBACK — back-to-home navigationPagerInitialFocusRequestdirection: NavigationDirectionpageIndex: numberpreviousPageIndex: number — -1 when no previous page existsPagerAnimationCompletedRequestpageIndex: numberpreviousPageIndex: number — -1 when no previous page existsPagerHandle: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.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.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.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.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.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.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.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.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.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.role, aria-label, and aria-roledescription. The standard aria-label attribute takes precedence over the ariaLabel alias.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.inert and aria-hidden, which keeps focus and assistive technology within the visible page.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.PagerPage for static lifecycle behavior and usePagerPageLifecycle for callbacks that depend on state or refs inside the page.onRequestInitialFocus when the page has a more appropriate target.aria-label so assistive technology announces a specific sequence rather than a generic page viewer.shouldPreventNavigation to block a controlled index change; controlled navigation bypasses the page veto by contract.homeIndex and keep useBackButtonForHome on. Verify both system Back on the device and Escape during desktop testing.preloadPageIfNeeded only after measuring a loading or transition issue rather than using it as a default performance measure.unloadPage for the visible page because the method throws.