PageTransition

Updated: Sep 24, 2026
Use PageTransition to animate changes between routes. Provide a transitionKey that identifies the current page and render that page as children. While the key remains stable, the host forwards updated children to the mounted page. When the key changes, the host keeps the outgoing page mounted, animates the outgoing and incoming pages, and then removes the outgoing page.
PageTransition is router-agnostic: it does not read history or import a router. The application or routing library owns the transition key and direction. With React Router, prefer ReactRouterPageTransition, which connects that routing state for the application.
The host owns DOM focus-and-scroll preservation during navigation. It captures scroll offsets and the focused control for each key, then restores them before a returning page becomes visible. This keeps focus and scroll context consistent across route changes. It does not capture React component state, router state, form models, pager or carousel indices, or other application state.
import type { ReactNode } from 'react';
import { PageTransition } from '@wearables-ui-toolkit/mrbd';
import type { PageTransitionDirection } from '@wearables-ui-toolkit/mrbd';

type RouteHostProps = {
  content: ReactNode;
  direction: PageTransitionDirection;
  routeKey: string;
};

export function RouteHost({ content, direction, routeKey }: RouteHostProps) {
  return (
    <PageTransition transitionKey={routeKey} direction={direction}>
      {content}
    </PageTransition>
  );
}
For a context switch between applications or top-level surfaces, use the larger cross-surface motion:
import {
  AppSwitchPageTransitionConfig,
  PageTransition,
} from '@wearables-ui-toolkit/mrbd';

<PageTransition
  transitionKey={surface.id}
  config={AppSwitchPageTransitionConfig}
>
  {surface.content}
</PageTransition>;
For an unvisited page, point initial focus to the control that best continues the user’s task:
import { PageTransition } from '@wearables-ui-toolkit/mrbd';

<PageTransition
  transitionKey={routeKey}
  initialFocus={{ selector: '[data-primary-action]' }}
>
  {content}
</PageTransition>;

Props

The complete prop type is PageTransitionProps. It extends standard div attributes except children and onTransitionEnd, which it redeclares. The component forwards all other attributes to the viewport element.
PropTypeDefaultDescription
transitionKey
string
—
Required. Identifies the current page. A change starts a transition.
children
ReactNode
—
Required. Content of the current page.
config
PageTransitionConfig
DefaultPageTransitionConfig
Defines the enter and exit motion.
direction
PageTransitionDirection
'forward'
Selects config.forward or config.back.
disabled
boolean
false
Replaces the page immediately without animation. onTransitionEnd still fires.
initialFocus
PageTransitionInitialFocus
'top-left'
Selects focus for a page with no saved state. The restoration pass resolves it, so it has no effect when preservePageState is off.
preservePageState
boolean
true
Captures and restores scroll offsets and the focused control for each key.
timingScale
number \| 'auto'
'auto'
Multiplies every duration, delay, and settle time.
onTransitionEnd
() => void
—
Called after the outgoing page is removed.
className
string
''
Class added to the viewport element.
pageClassName
string
''
Class added to each page layer.
style
CSSProperties
—
Inline styles merged over the generated transition custom properties.
The component installs capture handlers for focus, mouse down, and pointer down to track the most recently interacted control. The caller’s onFocusCapture, onMouseDownCapture, and onPointerDownCapture handlers still run.

Types and enums

PageTransitionDirection is 'forward' | 'back'.
PageTransitionInitialFocus supports four forms:
ValueBehavior
'top-left'
Default. Focuses the focusable element nearest the page’s top-left corner.
'none'
Leaves focus unchanged so the page can manage it.
{ selector: string }
Focuses the first matching element within the page.
(pageElement: HTMLElement) => HTMLElement \| null
Lets the caller resolve the target.
The package ships these configs:
ConfigUse
InAppPageTransitionConfig
Route changes within one application. It defines both directions with a short horizontal slide crossed with a fade and no scaling.
AppSwitchPageTransitionConfig
Switches between applications or top-level surfaces. It is longer and larger, and adds scale so the outgoing surface recedes. Forward only.
DefaultPageTransitionConfig
Alias for InAppPageTransitionConfig and the default when config is omitted.
PageTransitionConfig, PageTransitionMotionConfig, PageTransitionStateConfig, PageTransitionValueSegmentConfig, and PageTransitionSegmentConfig describe a config. Each of alpha, scale, and translateX animates as a separate segment with its own delay, duration, and easing. Separate segments can, for example, start a page’s slide before its fade. totalDurationMs is the settle time for the full transition, not the sum of its segments. When direction is 'back' and the config has no back entry, the component uses its forward motion.
Route navigation documents the complete config shape and how to derive a custom config from a shipped one.

Behavior

Layout. The viewport fills its parent and clips its content. The application must provide a parent with resolved dimensions, typically the application content area, so the pages have a stable responsive layout. During animation, the host absolutely positions and stacks both pages.
Sequence. The first render does not animate; the host starts settled on its initial key. For each later key change, both pages first render at their start values in a preparing phase so the browser can rasterize them. After one animation frame, the host waits for images in the incoming page to load and decode. A short timeout bounds this wait so a slow image cannot stall navigation. On the next frame, the enter and exit animations begin. After the config’s totalDurationMs, adjusted by the resolved timingScale, the host unmounts the outgoing page and calls onTransitionEnd.
Mid-flight changes. Updating children during a transition updates only the incoming page; the outgoing page retains the content it had when the key changed. Another transitionKey change restarts the transition toward the newest key. Timing-related prop changes do not restart an active transition, which finishes with its original timings.
timingScale. The component clamps a numeric value between one quarter and four times the configured timings. A value that is not finite and positive falls back to 1. The default 'auto' uses the configured timings in an ordinary browser and extends them in embedded browser environments where the compositor needs a longer window to produce the same perceived motion.
Preserved focus and scroll. With preservePageState on, the host captures the scroll offset of every scrollable element and the control that had focus when a page is left. It stores one latest snapshot per transitionKey and restores both before a returning page becomes visible. Two browser-history entries that use the same key therefore share that snapshot; use a more specific key when they require independent restoration. This is not general application state persistence: store route data, form models, selections, and pager or carousel indices separately. The host also synchronizes the control’s focus state so it displays focused material instead of only a browser outline. The capture accounts for timing races: if a click triggers navigation before focus moves, it restores the clicked control; if the browser has already dropped focus, it uses the page’s most recently focused control.
Restoration is positional. The host identifies scrollable elements by their child-index paths and the focused control by its index in the page’s eligible focus order, rather than by element identity. If the DOM structure or focusable order changes between visits, exact scroll restoration can fail and focus can land on a neighboring control. Native-disabled controls and controls with aria-disabled="true" are excluded from the saved focus order and from initial focus, even though a disabled InteractableBase remains reachable through ordinary directional navigation. Prefer stable page structure and focus order across visits. When either must change, consider turning preservePageState off and letting the page own focus placement.
Initial focus.initialFocus applies only when the current key has no saved snapshot. The resolved element must be focusable and eligible. The host skips a control with initialFocusEligible={false}. If a selector or function resolves to an ineligible element, focus remains unchanged. When focusable content loads lazily, the host retries on the next render, so a Suspense fallback does not prevent initial focus.
Reduced motion. When prefers-reduced-motion: reduce is active, the shipped stylesheet reduces enter and exit animations to a negligible duration. Pages therefore change without sliding or fading. The host still retains the outgoing page for the configured settle time, so onTransitionEnd timing does not change.

Accessibility

  • Every page except the topmost page is aria-hidden and inert. This is required so only the incoming page can receive focus, pointer input, or assistive-technology navigation.
  • The host adds no role, landmark, or accessible name. The page content owns its route name, typically through Page, so assistive technology receives the correct structure.
  • The host restores focus, or resolves initialFocus, before revealing the page. This prevents screen readers from landing at the document start instead of the intended control.
  • Prefer deriving direction from the navigation semantics rather than the trigger’s screen position. A route that animates back while adding a history entry communicates the wrong relationship to the user.

Application responsibilities

  • Give each page a stable transitionKey. Prefer sharing a key between two states of the same page so they update without animating against each other.
  • Derive direction from history position or navigation semantics to keep the motion consistent with the route relationship.
  • Prefer the host’s preservePageState behavior over separate scroll and focus restoration. Store route data, form models, selections, and pager or carousel indices in application or router state.
  • Start with a shipped config and override only the segments the experience needs.
  • Provide a parent with resolved dimensions so the host can fill it responsively.

Avoid this pattern

  • Avoid deriving direction from the triggering control’s screen position; that position does not describe the navigation relationship.
  • Do not render a second PageTransition for the same route level because each host would compete to own the route transition and page state.
  • Avoid adding custom route-change CSS animation on top of the host, which can produce conflicting motion.
  • Use initialFocus only with preservePageState; the same restoration pass resolves it.
  • Avoid changing focusable order between visits when exact focus restoration is important because restoration is positional.
  • Prefer relative page-layer offsets over hardcoded device dimensions so motion remains responsive.