refactor(banner): make hidden content inert and fix live region semantics

Author: lukemorawskiCreated Aug 18, 2026Updated Aug 18, 2026

Part of #4990

Clean up Banner accessibility so hidden content is actually hidden/inert, live regions behave consistently across platforms, and animation callbacks only fire when a real show/hide transition happens.

Banner doesn't exist in MD3 anymore, but it's still a supported Paper extension, so this is mostly about making its behaviour internally sane and accessible rather than matching the Material spec exactly.

Problems

  • Hidden Banner content is still mounted and reachable. It's only translated off-screen and clipped — screen readers can still see it and action buttons stay tabbable. Verified in the browser a11y tree: with visible={false}, both actions still have tabIndex: 0 and can receive focus.
  • role="alert" + aria-live="polite" doesn't really make sense. alert normally implies assertive + atomic, while Chrome currently ends up with alert atomic live="polite".
  • The live region currently does nothing on Android. RN maps aria-live to accessibilityLiveRegion on View, but not on Text, so the prop is effectively ignored there. It only works on web today.
  • iOS has no live-region equivalent, so the message isn't announced there either.
  • actions is unlimited and actions always sit below the message. Material allows max two.
  • If a focused action disappears after the actions array changes, focus gets dumped at the top of the page.
  • onShowAnimationFinished / onHideAnimationFinished fire on mount and again when theme.animation.scale changes, even though no visibility transition happened. Existing tests already call this out as probably a bug.

Focus Areas

  • Make hidden content properly inert: no pointer interaction, no a11y exposure, no focus.
  • Put the live region on a View so Android actually supports it, and keep it scoped to the message so action labels don't cause the whole banner to re-announce.
  • Make role and aria-live agree: polite by default, assertive when explicitly requested.
  • Announce manually on iOS without double-announcing on Android/web.
  • Limit actions to two and let them reflow depending on available width.
  • Restore focus if the currently focused action disappears.
  • Run show/hide animations and callbacks only for actual visible transitions.

Proposed API

One new prop, everything else is behavioural:

javascript
<Banner visible urgent actions={[...]}>
  Your payment failed.
</Banner>
  • urgent?: boolean
    • false (default): role="status" + aria-live="polite"
    • true: role="alert" + aria-live="assertive"; on iOS it interrupts instead of queueing

Breaking changes

  • Banner children are unmounted once fully hidden, so local child state is lost after hiding.
  • More than two actions are ignored, with a dev warning.

Notes

  • #5016 retokenizes Banner (motion tokens + easing), but doesn't touch any of this accessibility behaviour.
  • This should be checked separately with VoiceOver, TalkBack and the browser accessibility tree. They're different enough that testing only one isn't very meaningful.

Source: callstack/react-native-paper