Accessibility, performance, and validation

Updated: Sep 24, 2026
Validate interfaces for Meta Ray-Ban Display glasses with directional navigation, assistive technology, low-power device hardware, and desktop Chrome. Unit tests alone do not verify these user-visible behaviors.

Semantics provided by the toolkit

Prefer toolkit components over local reimplementations because they include the semantics described below.

Interactable roots

InteractableBase — and therefore every interactive toolkit component — resolves semantics like this:
SituationResult
Clickable, rendered as a plain div
Implicit role="button".
Rendered as a native element or a component such as a router Link
No role is injected; the element keeps its own semantics.
An explicit role prop
Always wins.
Focusable
tabIndex is 0 unless you pass a different tabIndex.
Not focusable, non-div root
tabIndex is forced to -1 so the native element leaves the tab order.
interactive={false} on a div root
No role, no tabIndex, no aria-disabled, and no interaction state at all.
disabled
aria-disabled is set, activation is suppressed, and the element stays focusable so users can still reach it and learn why.
Pass either ariaLabel or the standard aria-label. ariaLabel maps to the rendered aria-label only when the standard prop is absent.
Disabled controls remain reachable so users can discover them and understand their state. Pair disabled with tooltipMode={TooltipMode.DISABLED_CLICK} and tooltipText to explain why the control is unavailable when pressed; the toolkit dismisses the tooltip automatically.

Component semantics

ComponentShipped semantics
Page
The route root is a role="main" landmark, named from the header text and metadata. Pass role or aria-label to override either. Render one Page per route so there is exactly one main landmark.
SubNavigationPager, Pager
The pager root is a labeled container — role="group" for SubNavigationPager, role="region" for Pager — with a generic fallback name, so give it an ariaLabel. SubNavigation renders role="tablist" with aria-activedescendant and aria-keyshortcuts, and announces the new page position and label through a polite live region on every section change.
ListItem
Role resolves to slider, switch, or radio when the row shows that control, otherwise button. Checkable rows set aria-checked; slider rows set aria-valuemin, aria-valuemax, and aria-valuenow. The accessible name is composed from the title, subtitle, timestamp, status indicator, and trailing tag; an aria-label (or ariaLabel) you supply is prepended to that composition rather than replacing it, so use it for a lead-in phrase, not to restate the row.
Scrubber
role="slider" with aria-orientation, aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext. It has a sensible default label; override it with aria-label. Decorative track and handle elements are aria-hidden.
Switch, RadioButton, SliderBar
Controlled visual indicators. Switch and RadioButton handle no input, take no focus, and are never a tab stop, but still report role="switch" and role="radio" with aria-checked. SliderBar reports role="progressbar" with its range; passing it an onChange opts it into an interactive mode instead — role="slider", a tab stop, and arrow-key stepping by incrementPercentage. Because a role is always announced, putting one of these inside a row that already has switch, radio, or slider semantics produces two nested widgets for one action. Use ListItem’s showSwitch, showRadioButton, or showSlider slots, which move the semantics to the row and hide the indicator, instead of assembling a row by hand.
ProgressIndicator, ProgressRing, VolumeIndicator, ZoomIndicator
role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow, and a default label, on a non-focusable element. announceUpdatesForAccessibility adds a polite live region that announces the value as it changes — on by default for ProgressIndicator, off by default for the other three, so opt in when the value is the point of the screen.
IndeterminateLoader
role="progressbar" with no aria-valuenow, because the progress is unknown, and a default loading label you can override with aria-label.
VerticalMenu
role="menu" with a default label you can override. Its own buttons and any focusable child with role="menuitem" participate in arrow-key navigation; items that are aria-disabled, aria-hidden, or inert are skipped.
Modal
Forwards aria-label, falling back to a description composed from its own content.
Header
Combines the caller-supplied label, or the header text plus metadata, and always appends the avatar’s status description when one is shown.
IconImage and every icon slot
Rendered aria-hidden. Icons never carry the label; the host component does.
Tooltip popups
Rendered with role="tooltip" and an accessible label.
PageTransition
The outgoing page is marked aria-hidden and inert for the duration of the transition.

Keyboard and directional input

KeyBehavior
Enter, Space
Activate the focused interactable. The default action is suppressed, press visuals run, and one activation is dispatched.
Arrow keys
Move focus spatially within FocusNavigationProvider; if no target exists, scroll the nearest scroll container; if neither can move, run rubber-band feedback.
Tab
Native traversal is preserved by default. It is only suppressed when a FocusNavigationProvider sets blockTabNavigation.
Escape
Restores focus to the first eligible control when focus has left the provider or rests on its container element, and reaches the application boundary through useBackNavigation.
Preserve Tab navigation. The device uses directional input, but the same build runs in a browser where keyboard and assistive-technology users require a working tab order.

Application responsibilities

  • Give every icon-only control an explicit accessible label because title is not always rendered.
  • Name scrolling and paging containers. ScrollView and VerticalList render an unnamed role="region" landmark until the application passes an ariaLabel; Pager and SubNavigationPager otherwise use a generic name.
  • Give Avatar meaningful alt text unless the image is decorative.
  • Describe status indicators that communicate state. Built-in status types already add a description to the composed accessible name.
  • Context menus, vertical menus, and tooltips must remain reachable and dismissible through directional navigation and Escape.
  • Make the toolkit component the interaction target rather than wrapping it in another focusable element. A single target keeps focus behavior unambiguous.
  • Custom menu items must be focusable and use role="menuitem" so arrow-key navigation can include them.
  • Group related radio rows in a labeled container with role="radiogroup" so assistive technology communicates their relationship.

Text and localization

Design for text that changes with locale, dynamic data, and accessibility settings.
  • Let components size themselves, and use their supported max-line behavior so longer strings do not break layouts.
  • Keep toolkit text styles intact rather than globally compensating for text measurement differences.
  • Apply typography with TextAppearance classes or TextView’s textStyle. Do not set individual font properties or custom letter spacing on toolkit text; the toolkit owns these values for legibility and consistency.
  • The public text face is Noto Sans. App prefers a locally installed copy and otherwise loads it from the Google Fonts content delivery network at runtime, so no font package or bundled font files are required.
  • Use sentence case for actions and labels unless a product name requires a different form.

Performance

Optimize for the device’s low-power hardware. React re-renders, per-frame JavaScript, and additional compositing surfaces are the main costs.

React and state

  • Keep object and callback props stable with useMemo and useCallback in large lists and animated surfaces to avoid unnecessary rendering.
  • Avoid allocating new arrays or objects inline in list rows.
  • Do not drive a frame-rate visual animation from React state. Use CSS transitions, or use useSpringAnimation and write to the DOM from onChange. Its callback is capped at 30 updates per second by default, pauses while the document is hidden, and snaps to the target under reduced motion.

Materials and layers

  • Memoize one material per host and reuse it. Creating an instance on every render restarts transitions and rebuilds per-layer caches.
  • Reuse shape providers because the toolkit caches geometry by provider instance.
  • Keep animated material layers mounted across state changes. The canvas host already pauses its animation loop when a continuously animated layer is not visible in the current state.
  • Enable foregroundBlendsWithContent or backgroundBlendsWithBackdrop only when the blend needs live pixels because each option adds compositing surfaces.

Route loading

  • Split routes with createPreloadableLazyComponent and warm them through RoutePreloadProvider. The provider remains inert until it receives a preloadRoute handler. ReactRouterNavigationProvider mounts the provider and forwards its own preloadRoute. The toolkit queues requests, drains them on an idle callback, and defers them while directional input continues, preventing route warming from competing with scrolling or focus movement.
  • Place preload targets on content the user is likely to reach next, such as a focused row, hovered link, or item entering the viewport. Use RoutePreloadTarget, useRoutePreloadTarget, or ReactRouterPreloadLink.
  • Avoid application-level preloaders for unused materials or components because they consume resources without improving navigation.

Layout

  • Size layouts from available space and let components use their intrinsic dimensions. This keeps the interface responsive across viewports.
  • Keep one scroller per axis on each route. Nested same-axis scrollers make directional navigation ambiguous and increase scroll work.

Device browser compatibility

Validate the features most likely to differ between desktop Chrome and the device WebView:
  • SVG masks and clip paths
  • blend modes
  • filters and backdrop effects
  • font resolution and fallback
  • animated SVG content
  • large CSS or SVG layers
  • portal coordinate spaces
  • scroll and fading-edge compositing

Common pitfalls

  • Prefer full-screen captures when negative-stacked or masked material layers are present, then crop to the exact component bounds. Element-clipped DevTools screenshots can misrepresent these layers.
  • Treat anti-aliasing differences between desktop Chrome and the device WebView as rendering differences. Avoid changing component geometry to correct an aliasing-only discrepancy.
  • Wait for images and fonts to load before evaluating pixels because device transport can delay asset delivery.
  • Validate contrast and layering on hardware. The additive-light display draws content over the wearer’s view, so a design that reads against an opaque page background may not remain legible on the device.

Component-level validation

Compare the component itself rather than an entire screen:
  1. Render the component on a representative route or page.
  2. Match the state you intend to compare: default, focused, pressed, disabled, selected, loading, popup open, scroll position, route state, animation frame.
  3. Capture the full screen for context.
  4. Crop the exact component bounds.
  5. Compare that crop against the design specification.
  6. Record discrepancies in your validation notes.
  7. Add a regression test for anything you fix.
Compare equivalent states. A focused component is not a valid baseline for its default state, and a fading-edge overlay must be included when comparing a component beneath it.

Regression tests

Add focused tests for:
  • public API behavior and prop contracts
  • focus eligibility, focus restoration, and directional navigation outcomes
  • scroll-to-focused and fading-edge behavior
  • portal placement and popup anchoring
  • material configuration and layer ordering
  • layout calculations
  • which component owns an animation
  • edge cases that previously regressed
Prefer tests that protect user-visible behavior over implementation details. An assertion that only checks whether a mock ran does not establish the resulting behavior.

Validation checklist

Before shipping a change, verify:
  • The app renders inside App, which loads the library stylesheet.
  • Pages use Page for the header and content relationship.
  • Toolkit components are used for toolkit anatomy instead of local reimplementations.
  • No hardcoded colors, spacing, radii, typography values, or viewport dimensions — semantic CSS custom properties and the CornerRadius and TextAppearance scales only.
  • No custom font settings on toolkit component text.
  • No restyling of component internals, generated class names, or private DOM structure.
  • Every interactive element has an accessible name; icon-only controls have an explicit label.
  • Directional navigation reaches every control, and Tab still works.
  • Invalid directional input scrolls or rubber-bands instead of doing nothing.
  • Popups render through the app portal and stay anchored to their owner.
  • Reduced motion is honored.
  • The change renders acceptably in both desktop Chrome and the device WebView.
  • Component-level visual evidence exists for anything visually sensitive.