In React Native, how do you stop VoiceOver and TalkBack from reaching the inbox behind a custom bottom sheet drawn in the same view tree?
answer
- background still in the tree
- accessibilityViewIsModal: iOS, siblings only
- no-hide-descendants on Android
- aria-hidden sets both
- 'no' hides the view, not its children
basics
~10 sHide the background: aria-hidden={true} on the inbox container sets accessibilityElementsHidden on iOS and importantForAccessibility no-hide-descendants on Android; on iOS, accessibilityViewIsModal on the sheet also works, but only against the sheet's siblings.
solid answer
~40 sA sheet drawn over the inbox inside the same tree leaves the inbox views mounted, so a screen reader happily swipes behind the sheet. The cross-platform fix is `aria-hidden={true}` on the inbox container while the sheet is open: React Native turns it into `accessibilityElementsHidden` on iOS and `importantForAccessibility="no-hide-descendants"` on Android, hiding the container and everything inside it. The platform props also work: iOS-only `accessibilityViewIsModal` on the sheet makes VoiceOver ignore the sheet's siblings, but only siblings, so it must sit on the view that is the inbox's sibling; Android-only `importantForAccessibility` must be `no-hide-descendants`, because `no` hides only the container itself and leaves its children reachable. I clear the flags when the sheet closes, focus the sheet's first element when it opens, and give iOS an escape handler to dismiss it.
code
tsx · 29 linesimport {Pressable, Text, View} from 'react-native';
import type {ReactNode} from 'react';
type Props = {
inbox: ReactNode;
sheetOpen: boolean;
onClose: () => void;
};
export function InboxWithMoveSheet({inbox, sheetOpen, onClose}: Props) {
return (
<View style={{flex: 1}}>
<View style={{flex: 1}} aria-hidden={sheetOpen}>
{inbox}
</View>
{sheetOpen ? (
<View
accessibilityViewIsModal
onAccessibilityEscape={onClose}
style={{position: 'absolute', left: 0, right: 0, bottom: 0}}>
<Text accessibilityRole="header">Move to folder</Text>
<Pressable onPress={onClose}>
<Text>Cancel</Text>
</Pressable>
</View>
) : null}
</View>
);
}go deeper
Recall that content drawn behind a custom sheet is still reachable by a screen reader, and that aria-hidden on the background container hides it.
Explain the iOS and Android props aria-hidden maps to, the siblings-only rule of accessibilityViewIsModal, and why importantForAccessibility no is not enough.
Show the full cycle: hide the background, focus into the sheet, support the iOS escape gesture, and restore both the tree and focus when the sheet closes.
Decide whether the app's overlays should share one primitive that owns hiding, focus and dismissal, so each feature team does not reimplement the cycle differently.
## The problem A **custom bottom sheet**, say a "Move to folder" picker drawn with an absolutely positioned `View` rather than the core `Modal` component, sits on top of the inbox visually. In the view tree, though, the inbox is still there, mounted and accessible. A sighted user cannot tap through the sheet's backdrop, but a **VoiceOver** or **TalkBack** user swiping through elements walks straight past the sheet into the email rows behind it, with no indication that they have left the dialog. The background must be removed from the accessibility tree while the sheet is open. ## The props React Native gives you | Prop | Platform | Effect | |---|---|---| | `aria-hidden` | both | on `View`, React Native sets `accessibilityElementsHidden` and, when `true`, `importantForAccessibility="no-hide-descendants"` | | `accessibilityElementsHidden` | iOS | hides the view and every element it contains from VoiceOver | | `accessibilityViewIsModal` | iOS | VoiceOver ignores the elements in this view's **siblings** | | `importantForAccessibility` | Android | `auto`, `yes`, `no` or `no-hide-descendants` | ## Details that decide whether it works - **`accessibilityViewIsModal` works on siblings only.** The docs' example: in a window with sibling views A and B, setting it on B hides A. Setting it on a child C *inside* B does **not** hide A. So the flag must sit on the sheet's outermost view, and the inbox must be that view's sibling. Wrap the sheet in extra containers and the flag silently stops working. - **It is iOS-only.** TalkBack ignores it, so an app that relies on it alone is still broken on Android. - **`importantForAccessibility="no"` is not enough.** It marks only the view itself as unimportant; its children can still be reached. `no-hide-descendants` hides the view and everything inside it, which is what a background needs. - **`aria-hidden={true}` is the portable choice.** On a `View`, React Native maps it to both platform props, so one prop hides the inbox under VoiceOver and TalkBack. ## A complete open-and-close cycle 1. When the sheet opens, set `aria-hidden={true}` on the inbox container (or the platform props). 2. Put focus inside the sheet, typically on its title, so the user knows the context changed. 3. Give iOS users a way out: `onAccessibilityEscape` on the sheet lets the two-finger scrub gesture dismiss it, and a visible Cancel button serves everyone. 4. When the sheet closes, set `aria-hidden` back to `false`, so the inbox is reachable again, and return focus to the row that opened the sheet. Forgetting step 4 is a nasty bug: the inbox stays hidden from the screen reader after the sheet is gone, and the screen looks empty to a VoiceOver user. ## What not to do - Do not hide the background by unmounting it; you lose scroll position and state. - Do not set `accessible={false}` on the inbox container; that only changes whether the container itself is an element, it does not hide its children. - Do not rely on the backdrop's `Pressable`: it blocks touches for sighted users, not swipes between elements for screen-reader users. ## Where the core `Modal` fits The core `Modal` component presents its content separately from the screen beneath it, which is one reason teams reach for it; its props and behaviour belong to the overlay components rather than to these accessibility props. The props here are what you need whenever the overlay is drawn in the same tree: sheets, custom dialogs, toasts that demand a response, and full-screen menus.
- The team set accessibilityViewIsModal on the sheet's inner content view and VoiceOver still reaches the inbox. Why?The flag makes VoiceOver ignore the siblings of the view it is on. On an inner view, its siblings are other parts of the sheet, not the inbox, so nothing behind the sheet is hidden. Move the flag to the sheet's outermost view, the one that is the inbox container's sibling, or hide the inbox with `aria-hidden`.
- What goes wrong if aria-hidden is not reset when the sheet closes?The inbox stays out of the accessibility tree: it is visible on screen but VoiceOver and TalkBack find nothing to focus, so the app looks empty to a screen-reader user. Derive `aria-hidden` from the same state that shows the sheet, as in `aria-hidden={sheetOpen}`, so the two cannot disagree.
saying these in an interview costs you the question
- accessibilityViewIsModal on any view inside the sheet hides everything behind it.
- accessibilityViewIsModal also traps TalkBack focus on Android.
- importantForAccessibility='no' on the background hides all of its children.
- Setting accessible={false} on the inbox container hides the rows inside it.
- A full-screen backdrop Pressable stops screen-reader users from reaching the inbox.