[Bug]: Closed sheet's backdrop stays pointerEvents 'auto' and blocks all touches on devices with a fractional dp screen height (animatedIndex = -0.99999998, never -1)
Version
v5
Reanimated Version
v3
Gesture Handler Version
v2
Platforms
Android
What happened?
A permanently mounted, closed BottomSheet (index={-1}) with a BottomSheetBackdrop leaves an invisible, full-screen, pointerEvents: 'auto' backdrop on top of the whole screen on any device whose screen height in dp is not an integer — e.g. a Samsung Galaxy S23 Ultra / S21 FE at a non-default system "Display size" (1080×2340 @ density 2.625 → 891.4285… dp). Every React Native touch underneath it dies: Pressable, Switch, TextInput focus, ScrollView/FlatList scrolling, native pickers. gesture-handler controls keep working, because GH's own hit-testing skips views with alpha < 0.1 — which is why this got misdiagnosed for months as a React Native density bug (facebook/react-native#58360, now redirected here).
This is the same bug as #2680 ("Invisible closed backdrop can block touches after cold start on some Android devices", closed as stale — a commenter there already observed animatedIndex going to -0.9999… instead of -1) and the same "stale initial position" family as #2746. Here is the root cause with numbers.
Measured (5.2.9, but the code path is identical on master / 5.2.14) on the S23 Ultra at Display size 2.625, every mounted closed sheet after layout:
index=-0.9999999782638337 position=891.4285714285714position=Dimensions.get('screen').height(5.2.9:INITIAL_POSITION = SCREEN_HEIGHT; master line 218 usesDimensions.get('window').height) =2340 / 2.625computed as a JS double →891.4285714285714.- The container height the index math uses (
adjustedSnapPoints.push(containerHeight)in theanimatedIndexderived value) comes from the container'sonLayout→ a float32 from the native layout →891.4285888671875. - The two differ by ~1.7e-5 dp. Because a sheet that is never opened is never animated,
animatedPositionkeeps the stale initial constant and never gets re-synced toclosedDetentPosition.interpolate(position, [..., containerHeight], [..., -1], CLAMP)therefore lands at-0.99999998, not-1. BottomSheetBackdropgates touchability with an exact comparison,animatedIndex.value <= disappearsOnIndex(line 123).-0.99999998 <= -1is false →pointerEventsstays'auto'on a fully transparent full-screen view.animatedSheetState(animatedPosition.value >= closedDetentPosition) is likewise neverCLOSED.- On devices with an integer dp height (1080×2340 @ 3 → 780 dp) the double and the float32 are equal, the index is exactly
-1, and everything works — hence "only some devices".
Why the invisible backdrop kills native controls too, not just JS pressables: RN's TouchTargetHelper picks it as the JS touch target (it is the top-most view under the finger with pointerEvents: auto), and on Android a ReactViewGroup with pointerEvents: auto returns true from onTouchEvent, so it also consumes the native touch before Switch/EditText/ScrollView beneath it ever see it.
Affected in practice: any Android device where Dimensions.get('screen').height is fractional. Samsung's Display-size presets produce densities like 2.625 and 2.8125 on 1080-wide panels (2340/2.625, 2400/2.8125 = 853.33 — the device in #2746). It is silent, app-wide, and looks like "the app is frozen except gesture-handler buttons".
Suggested fix (any one of these closes the gap; the first two are the real fix):
- Re-sync
animatedPositiontoclosedDetentPositionwhenever detents are (re)computed while the sheet is closed (index === -1/ never opened), so the closed position and the container height are the same float. - Or initialise the position from the measured layout rather than a
Dimensionsconstant (the constant is also what produces the status-bar peek in #2746). - Defensively, gate the backdrop with a tolerance (
animatedIndex.value <= disappearsOnIndex + 1e-3) or onanimatedSheetState === CLOSED, instead of an exact float comparison.
Workaround we ship (in case it helps others): wrap the backdrop in a View whose pointerEvents is 'none' unless our own controlled visible state says the sheet is open or still animating closed. The disappearsOnIndex={-0.5} trick from #2680 also works.
Reproduction steps
- Use a device (or emulator) whose screen height in dp is not an integer. Emulator: any 1080×2340 skin, then
adb shell wm density 420(→ 891.43 dp). Real device: Samsung Galaxy S23 Ultra or S21 FE, Settings → Display → Screen zoom at any non-default position. - Run the Snack below. It mounts a closed
BottomSheet(index={-1},snapPoints={['50%']}) withBottomSheetBackdrop(appearsOnIndex={0},disappearsOnIndex={-1}) and shows the backdrop'sanimatedIndex/animatedPositionplus a plain RNButtonandSwitch. - Observe the panel:
animatedIndex = -0.99999998,backdrop pointerEvents = 'auto'. Tap the Button, flip the Switch: nothing happens. - Set the density back to the panel default (
adb shell wm density reset, or Screen zoom to default) and relaunch:animatedIndex = -1,pointerEvents = 'none', Button and Switch work.
Reproduction sample
Relevant log output
# @gorhom/bottom-sheet 5.2.9, RN 0.86.2 (Fabric), reanimated 4.5.3, gesture-handler 2.32.0
# Samsung SM-S918B (Galaxy S23 Ultra), Android 16 / One UI, Display size => density 2.625
# env: window 411.4x891.4 @2.625, screen 411.4x891.4 @2.625
# right after mount (layout not calculated yet):
sheet "App Update" index=-1 position=891.4285714285714 gorhomPointerEvents=none
# after layout — every mounted closed sheet:
sheet "Transits" index=-0.9999999782638337 position=891.4285714285714 gorhomPointerEvents=auto
sheet "Colors" index=-0.9999999782638337 position=891.4285714285714 gorhomPointerEvents=auto
sheet "Wisdom" index=-0.9999999699037697 position=891.4285714285714 gorhomPointerEvents=auto
sheet "App Update" index=-0.9999999673957506 position=891.4285714285714 gorhomPointerEvents=auto
# a tap on a plain RN Pressable in a screen; JS target resolved by React Native, owner chain via fiber walk:
ROOT touchStart page=(288.1,847.9) target=262 owner=AnimatedComponent(View) < Wrap < AnimatedComponent(Wrap) < Wrap < GestureDetector < BottomSheetBackdropComponent < BottomSheetGestureHandlersProvider < BottomSheet < BottomSheet < AppBottomSheet < UpdateAvailableSheet
# same device at the default Display size (density 3, 780 dp): index=-1, pointerEvents=none, all touches work.Source: gorhom/react-native-bottom-sheet