Skip to content

fix(android): keep an open sheet interactive with a transparent scrim - #82

Open
giaBaoJS wants to merge 1 commit into
software-mansion-labs:mainfrom
giaBaoJS:fix/android-transparent-scrim-interactivity
Open

fix(android): keep an open sheet interactive with a transparent scrim#82
giaBaoJS wants to merge 1 commit into
software-mansion-labs:mainfrom
giaBaoJS:fix/android-transparent-scrim-interactivity

Conversation

@giaBaoJS

@giaBaoJS giaBaoJS commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The bug

On Android the host's interactivity is derived from the scrim's resolved
opacity:

internal val isInteractive: Boolean
  get() = modal && (activeAnimation != null || isPanning || isScrimVisible())

private fun isScrimVisible(): Boolean = modal && scrimProgress > 0.001f

scrimProgress comes straight from the public scrimOpacities prop, so a
modal sheet configured with no dim reports itself non-interactive the moment
it finishes opening. updateInteractionState() then puts the host into
BOX_NONE and, in nativeOverlay mode, OverlayCloseRequestController
adds FLAG_NOT_TOUCHABLE to the dialog window and drops its alpha to 0 when
there is no onCloseRequest handler.

It is sticky: recovering needs another emitPosition(), and the touch-driven
producers of one are exactly what the flag just disabled.

Reproduction

<ModalBottomSheet
  nativeOverlay
  detents={[0, 300]}
  index={index}
  onIndexChange={setIndex}
  scrimOpacities={[0, 0]}
  surface={<View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />}
>
  <Pressable onPress={() => setIndex(0)}>
    <Text>Close</Text>
  </Pressable>
</ModalBottomSheet>

Set index to 1 on Android. The sheet animates in (interactive while the
spring runs) and then vanishes at settle, because the overlay window goes to
alpha 0. Add an onCloseRequest handler and it stays visible but ignores
every touch instead. The same happens for any scrimOpacities entry that is
0 at an open detent, for example [0, 0, 1] with detents={[0, 200, 600]}.

Root cause

Scrim opacity is presentation styling, not a signal that the sheet is on
screen, so a legal scrimOpacities value silently switched off input for an
open sheet. iOS does not share this: point(inside:) returns true for
presentedSheetFrame unconditionally and only the area outside the sheet is
gated on isScrimVisible.

This tracks the resolved target detent and the on-screen sheet height
instead. With the default opacities (0 at closed detents, 1 otherwise) the
predicate is equivalent to the previous one, so only the transparent-scrim
configurations change. Scrim taps keep following scrim visibility, which is
what iOS does too.

Test

open native overlay stays interactive with a fully transparent scrim opens
a nativeOverlay sheet at its 300pt detent with scrimOpacities of
[0f, 0f] and asserts the dialog window is touchable and at alpha 1. Without
the source change it fails with expected:<0> but was:<16>
(FLAG_NOT_TOUCHABLE). animateIn is false in the fixture, so nothing in
the test depends on an animation completing.

./example/android/gradlew --project-dir example/android :swmansion_react-native-bottom-sheet:testDebugUnitTest --rerun-tasks
goes from 65 to 66 tests, 0 failures. bun run typecheck and bun run lint
are clean.

The host derived its interactivity from the scrim's resolved opacity, so
a modal sheet whose scrimOpacities are 0 at the open detent reported
itself non-interactive once it settled. In nativeOverlay mode that adds
FLAG_NOT_TOUCHABLE to the dialog window, and drops its alpha to 0 when
there is no onCloseRequest handler, so an open sheet stops receiving
touches or disappears outright. Follow the resolved target detent and
the on-screen sheet height instead, matching iOS, where the sheet stays
hit-testable and only the scrim area follows scrim visibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant