Navigation and focus

Updated: Sep 24, 2026
Design applications for Meta Ray-Ban Display glasses for directional navigation rather than pointing. One element owns focus, arrow input moves focus between elements based on their on-screen position, and confirm activates the focused element. This model determines how routes, scrollers, and overlays work together.

The focus model

App installs a focus-navigation provider scoped to the application content root. When focus is inside that root, the provider resolves arrow input in this order:
  1. It searches in the requested direction for the nearest eligible element, beginning in the innermost focus section that contains the current element and then widening outward. A candidate must be rendered, visible, not inert, not aria-hidden, and not clipped from view by an ancestor that cannot scroll to reveal it.
  2. If no element qualifies, it asks the owning scroll container to move in that direction when distance remains.
  3. If neither focus nor scrolling can move, it sends the focused element an invalid-direction signal, which produces boundary rubber-band feedback.
If an arrow event arrives while focus is outside the provider container, the provider focuses the first eligible element instead of moving focus. This gives directional input a valid destination.
Native Tab traversal remains available by default so keyboard and assistive technology users are not trapped. Use a separate FocusNavigationProvider with blockTabNavigation only for an intentionally arrow-only experience.
When focus rests on the provider container, Escape returns focus to the first eligible descendant. This supports desktop keyboard testing; it is not the device Back path. The provider does not consume the event, so application-level Back handling can still run.
Where a real mouse is expected, a primary-button click near a focusable element moves focus to the nearest focusable, making mouse input behave like directional navigation. The browser handles a click directly on or inside a focusable element. This proximity behavior is disabled on Meta Ray-Ban Display, where synthetic taps must not move focus.
When application logic needs to move focus, use the focus-navigation hook:
import { useFocusNavigation } from '@wearables-ui-toolkit/mrbd';

const { moveFocus, activateFocused, clearFocus, isActive } = useFocusNavigation();
MemberBehavior
moveFocus(direction)
Runs one directional search for 'up' \| 'down' \| 'left' \| 'right'; returns whether anything moved.
activateFocused()
Activates the element that currently has focus.
clearFocus()
Focuses the first eligible element when focus has left the provider’s container or rests on the container itself. Does nothing while a descendant control owns focus.
isActive
Whether focus navigation is enabled.
Programmatic focus must produce the same visual state as focus reached with an arrow key. These APIs and each component’s focus handling maintain that invariant. Do not simulate key events to move focus, because synthetic input bypasses that ownership.

Initial focus

App enables automatic initial focus. After a route settles, the focus coordinator selects the top-left eligible interactable in the active root. It leaves an existing page focus owner unchanged and skips elements on a page that is animating out.
  • Place the route’s primary task first so composition selects the intended initial target. A route centered on navigation can begin with navigation.
  • Toolkit interactables are eligible for initial focus by default. Set initialFocusEligible={false} on an element that must not become the entry point.
  • Keep the initial target completely visible in the first settled frame. If a long static introduction pushes it out of view, condense that content, make an earlier element interactive, or move the action to the bottom dock.
  • Animate only the initial target into its focused state on load, using the same transition used during navigation. This preserves a single, unambiguous focus owner.
When PageTransition has no saved focus to restore, it selects initial focus for the page entering the viewport. Its initialFocus prop defaults to 'top-left' and also accepts 'none', { selector }, or a function that receives the page element and returns the element to focus.

Partial focus and handoff

Let components own boundary feedback and the focus-material handoff between interactables. Configure that behavior through the supported props rather than recreating its visual effects.
PropDefaultPurpose
partialFocusSupportedAxis
PartialFocusSupportedAxis.XY
Which axes give rubber-band and handoff feedback. Also X, Y, and None.
isRubberbandTranslationEnabled
true
When false, rubber-band feedback updates the material origin without translating the element.
onInvalidFocusDirection
—
Called with the direction when directional input could not move focus.
onPartialFocusHandoff
—
Called when focus moves between interactables so the material can render the handoff.
Avoid adding scale or opacity effects around a toolkit component to imitate this behavior. Competing effects obscure the component’s focus state.

Scrolling and focus

Treat focus and scrolling as one directional interaction. Assign one scroll container per axis in a route and let that container own movement and reveal.
  • Focusing the first or last element places the scroller at its absolute start or end when that element can remain fully visible and clear of the active fading edge and header inset. ScrollView verifies this before aligning the boundary. If non-focusable content before or after the control would move it outside the safe area, focus reveal takes priority; continued outward arrow input scrolls the remaining content incrementally.
  • When focus has no eligible destination but the scroller has distance left, the scroller advances by a computed step instead of a full viewport.
  • Directional navigation continues while any part of the focused element remains in the physical viewport. Focus can therefore move to a partially visible neighbor, whose normal reveal then clears fading edges and the header. If incremental scrolling moves the current element completely outside the viewport, the opposite arrow first scrolls it back into view and then moves to a neighbor.
  • Fading edges communicate remaining distance. Their strength follows how much content remains hidden on that side, and the fade disappears at the true boundary. Avoid leading or trailing spacer elements and permanent fade clearance; the scroller owns this presentation.
  • Rubber-band feedback indicates that no distance remains. The scroller uses any remaining distance before showing that boundary response.
  • A focused ScrollView also responds to PageUp, PageDown, Home, and End.

Back

System Back is the primary Back input on Meta Ray-Ban Display and follows browser-history behavior. During desktop testing, Escape exercises the same component and application-level Back path. The innermost layer that owns Back handles it so a lower layer does not act on the same request.
OrderLayerWhat happens
1
A focusable tooltip popup, including a VerticalMenu anchored with getVerticalMenuAnchorProps
Owns a temporary same-URL history entry. Back dismisses the popup and returns focus to its anchor before the previous application route can be reached.
2
An open menu that has focus
ContextMenu calls onDismiss(DismissReason.BACK_BUTTON); VerticalMenu calls onDismissRequest(VerticalMenuDismissReason.ESCAPE). Desktop Escape follows this same dismissal path.
3
Pager or SubNavigationPager
With useBackButtonForHome (default true), a homeIndex of zero or greater, and a current page that is not already home, returns to the home page instead of leaving the route. On the home page it claims nothing, so Back falls through.
4
The application
Your useBackNavigation handler, or the router adapter, handles route return.
The application owns Modal visibility; Modal does not claim Back automatically. Close it from the application’s Back handler and return focus to the control that opened it, preserving the user’s navigation context.
Register useBackNavigation once at the application boundary for any Back request that reaches the route. Return false when navigation cannot continue so the request remains unclaimed. Any other return value claims it. On desktop, an unclaimed Escape key runs the same handler.
import { useBackNavigation } from '@wearables-ui-toolkit/mrbd';

useBackNavigation(() => {
  if (!canGoBack) {
    return false;
  }
  goBack();
});
For React Router, ReactRouterNavigationProvider connects application-level Back to router navigation. System Back traverses browser history; desktop Escape and imperative navigateBack() calls use the same route-return and restoration path. At the first history entry, the provider navigates to fallbackTo ('/' by default). A control that needs the same behavior can read it directly:
import { useReactRouterNavigation } from '@wearables-ui-toolkit/mrbd/react-router';

const { navigateBack } = useReactRouterNavigation();
Applications must not render an in-application Back control or intercept Back in page code. Use the routed Back path so history and restoration behavior stay consistent across the application.

Restoration

Use an ordinary forward transition to push a history entry, and reserve replace for genuine redirects. Before a returning page is revealed, restore the route, focused element, visual focus state, scroll offset, and any pager or carousel index. Focus paired with a stale scroll offset can leave the restored target outside the visible rectangle, so these values must be restored as one state.
PageTransition handles the DOM focus-and-scroll portion of this lifecycle. It retains outgoing and incoming pages during the animation. With preservePageState (true by default), it captures scroll offsets and one positional focus index in the latest snapshot for each transition key, then restores both before showing a returning page. It does not preserve router data, React component state, form models, pager or carousel indices, or separate snapshots for multiple history entries that share a key. When no snapshot exists, initialFocus chooses the focus target.
The application owns two remaining pieces:
  • Give the transition its own location snapshot and pass that snapshot to the route switch. This ensures Back renders the retained page rather than the first matching route. Application Structure shows the complete shell.
  • Store return state such as a pager index or selection outside the route. A detail route’s local state disappears when that route unmounts.
An embedded browser can move focus to the document root during same-document navigation, which would clear the focused control’s visual state. preserveFocusedInteractableDuringNavigation() opens a short retention window that holds the state and returns a function that closes the window early. Open the window when navigation begins and close it after the destination appears; PageTransition’s onTransitionEnd is the typical release point. Do not release it in the same tick as navigation, because the window would close before the browser moves focus. The window expires automatically, so a missed release does not leak.
import { useRef } from 'react';
import { preserveFocusedInteractableDuringNavigation } from '@wearables-ui-toolkit/mrbd';

const releaseRef = useRef<(() => void) | null>(null);

function startNavigation(nextPath: string) {
  releaseRef.current?.();
  releaseRef.current = preserveFocusedInteractableDuringNavigation();
  navigate(nextPath);
}

function handleTransitionEnd() {
  releaseRef.current?.();
  releaseRef.current = null;
}

Pager and SubNavigationPager

Treat a pager as a second navigation axis within one route. It has its own focus and lifecycle contract.
Pager uses arrow input along its orientation to move between pages and reports each entry with a NavigationDirection. A page declares lifecycle behavior by wrapping content in PagerPage or calling usePagerPageLifecycle from within the page.
Lifecycle memberBehavior
onRequestInitialFocus(request)
Return true to claim initial focus for the incoming page; otherwise the pager focuses that page’s first focusable.
shouldPreventNavigation()
Return true to veto a keyboard- or focus-driven page change.
onWillShowPage / onWillHidePage
Fire around the page becoming or ceasing to be current.
onWillUnloadPage
Fires before an inactive page is unmounted under unmountInactivePages.
onAnimationCompleted(request)
Fires after an animated transition into the page finishes.
onWillInterceptBack()
Return true to consume Back before the pager’s own back-to-home handling.
Two defaults affect ownership. requestInitialFocusOnMount is false, so the first page does not claim focus unless requested. A change through the controlled currentPageIndex prop is forced navigation and bypasses shouldPreventNavigation, because the application owns that state. If a page needs to veto such a change, gate the state update in the application. Set navigationLocked to reject requested page changes, and use onNavigationAttemptWhileLocked to communicate why. Page-set reconciliation can still move the resolved index when the previous page no longer exists.
SubNavigationPager adds a tab bar and coordinates focus with the current page:
  • Selecting a tab while the tab bar owns focus changes the page and keeps focus on the bar, allowing consecutive tab changes without dropping into content.
  • A page change initiated outside the tab bar focuses the incoming page as usual.
  • ArrowDown from the tab bar focuses the first focusable element in the current page.
  • A scrim fades in behind the focused tab bar to keep labels legible over page content.
  • autoHide defaults to false. When enabled, it fades the bar after inactivity and restores it on focus.
  • Give every control a useful neighbor in each direction a user is likely to press, supporting predictable directional navigation.
  • Keep exactly one visible focus owner so the next action is unambiguous.
  • Use component-provided focus, partial-focus, and handoff behavior to maintain consistent feedback.
  • Return focus to the control that opened an overlay when the overlay closes.
  • Test Back through the host’s routed Back path in both directions, and verify the restored frame before it is revealed.

Avoid these patterns

  • Avoid raw key-event interception for custom navigation; it bypasses the provider’s focus and scroll coordination.
  • Do not add a focusable wrapper around a focusable component or place focusable children inside a focusable container. Nested focus owners create ambiguous navigation and accessibility semantics.
  • Animate only the selected initial target into focus rather than animating every control on load.
  • Avoid custom scale or opacity effects that imitate the toolkit focus, because they can conflict with component-owned state.
  • Every interactive route must expose an eligible initial-focus target so directional input has a valid starting point.