I ship an Expo consumer mobile app for creator workflows. Operators tap Accept and Bulk Accept dozens of times a day inside confirmation dialogs. Our design-system Button sits on react-native-gesture-handler GestureDetector, not plain Pressable. On list screens and detail screens those buttons fire. Inside React Native Modal dialogs they go dead. No redbox, no log line, just silence. Android was worse: taps felt like they hit glass. iOS sometimes swallowed the first press. Staging needed an OTA before production noticed because QA on flat screens never opened the modal flows first.
The trap is comforting. The app root already wraps the tree in GestureHandlerRootView. Every screen-level button proves gestures work. So when dialog buttons fail, you grep the button component, not the modal shell. That is the wrong file.
Modal is a second native window
React Native Modal does not render as a child of your main React tree on the native side. It opens a separate window. Everything inside that window sits outside the root GestureHandlerRootView you declared in App.tsx. GestureDetector needs that root to deliver touch events into the gesture system. Without it, taps evaporate. The button still paints. It just never calls onPress.
That is why the same Button works on a scroll screen and dies in a dialog. The component is fine. The gesture root never followed the modal content into the second window.
Wrap every modal content tree
The fix is structural, not a prop on Button. Nest GestureHandlerRootView as the first child inside every RN Modal you own: dialogs, media viewers, image lightboxes, anything that uses Modal from react-native.
import { Modal as RNModal } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
<RNModal visible={visible} animationType='fade' transparent>
<GestureHandlerRootView style={{ flex: 1 }}>{/* dialog chrome + GestureDetector buttons */}</GestureHandlerRootView>
</RNModal>;style={{ flex: 1 }} matters when the modal is transparent or full-screen. Without flex the root can collapse and layout looks fine while hit targets still misbehave on some devices.
I audited every Modal import after the staging fix. Shared dialog primitives got the wrapper once so product screens did not each remember it. Media viewers and full-screen image previews use the same rule. If it mounts through Modal, it gets its own gesture root before any GestureDetector child.
The failure mode is silent by design. React Native does not warn that modal content left the gesture hierarchy. You only notice when revenue workflows stall and operators assume the app froze.
Why Jest stayed green
Our unit tests mock GestureDetector as Pressable. Modal tests therefore passed while real devices failed. A structural test catches the regression: render the dialog with visible, query for a gesture root test id (or assert RNModal descendants include GestureHandlerRootView), and fail if the wrapper is missing.
Device QA on iOS and Android still belongs in the checklist. The mock hides the native window split. CI can only guard the tree shape.
Once I internalized "modal equals new window," dead Accept buttons stopped feeling like a button bug. They were a missing gesture root in a place the app root never reached.
Happy coding! Sander