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:
<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
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
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".aria-livetoaccessibilityLiveRegiononView, but not onText, so the prop is effectively ignored there. It only works on web today.actionsis unlimited and actions always sit below the message. Material allows max two.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
Viewso Android actually supports it, and keep it scoped to the message so action labels don't cause the whole banner to re-announce.roleandaria-liveagree: polite by default, assertive when explicitly requested.visibletransitions.Proposed API
One new prop, everything else is behavioural:
urgent?: booleanfalse(default):role="status"+aria-live="polite"true:role="alert"+aria-live="assertive"; on iOS it interrupts instead of queueingBreaking changes
Notes