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 havetabIndex: 0and can receive focus. role="alert"+aria-live="polite"doesn't really make sense.alertnormally implies assertive + atomic, while Chrome currently ends up withalert atomic live="polite".- The live region currently does nothing on Android. RN maps
aria-livetoaccessibilityLiveRegiononView, but not onText, 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.
actionsis 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/onHideAnimationFinishedfire on mount and again whentheme.animation.scalechanges, 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
Viewso Android actually supports it, and keep it scoped to the message so action labels don't cause the whole banner to re-announce. - Make
roleandaria-liveagree: 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
visibletransitions.
Proposed API
One new prop, everything else is behavioural:
<Banner visible urgent actions={[...]}>
Your payment failed.
</Banner>urgent?: booleanfalse(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