๐ŸŽ‰ True Sheet 4.0 is out with a rewritten layout engine. Read the announcement ยท Migration guide
Guides

Dismissing

Control how the bottom sheet is dismissed, block dismissal, and confirm unsaved changes.

The user can dismiss a sheet by dragging it down, tapping the dim, pressing the back button (Android), or pressing escape (Web). This guide shows how to block these gestures, confirm before the sheet closes, and change how far the user must drag.

Preventing Dismissal

Set dismissible to false to keep the sheet presented. The user can still drag it between detents, but it springs back when they drag it down. Close it from code with dismiss.

export const App = () => {
  const sheet = useRef<TrueSheet>(null)

  return (
    <TrueSheet
      ref={sheet}
      dismissible={false}
    >
      <Button title="Done" onPress={() => sheet.current?.dismiss()} />
    </TrueSheet>
  )
}

draggable={false} stops the user from dragging the sheet, but a tap on the dim still dismisses it. Use dismissible={false} to block both.

Confirming Dismissal

When dismissible is false, a blocked attempt fires onDismissAttempt. Use it to confirm before discarding unsaved changes, then call dismiss() to proceed.

export const App = () => {
  const sheet = useRef<TrueSheet>(null)
  const [isDirty, setIsDirty] = useState(false)

  return (
    <TrueSheet
      ref={sheet}
      dismissible={!isDirty}
      onDismissAttempt={() => {
        Alert.alert('Discard changes?', undefined, [
          { text: 'Keep editing', style: 'cancel' },
          { text: 'Discard', style: 'destructive', onPress: () => sheet.current?.dismiss() },
        ])
      }}
      onDidDismiss={() => setIsDirty(false)}
    >
      <TextInput onChangeText={() => setIsDirty(true)} />
    </TrueSheet>
  )
}

These count as an attempt:

  • Dragging the sheet down
  • Pressing the back button (Android) or escape (Web)
  • The accessibility dismiss action, e.g. the VoiceOver escape gesture

A tap on the dim does not count, matching iOS.

Native can't wait for JavaScript to decide, so you can't block a dismissal from a callback. Toggle dismissible from state before the user tries to dismiss.

On Android, onDismissAttempt consumes the back press while dismissible is false. Return false from onBackPress to let it propagate, e.g. to go back to the previous screen.

With Navigation

In the sheet navigator, set dismissible as a screen option and listen for sheetDismissAttempt. Call navigation.goBack() to close the sheet.

function EditSheet() {
  const navigation = useTrueSheetNavigation()
  const [isDirty, setIsDirty] = useState(false)

  useEffect(() => {
    navigation.setOptions({ dismissible: !isDirty })
  }, [navigation, isDirty])

  useEffect(() => {
    return navigation.addListener('sheetDismissAttempt', () => {
      Alert.alert('Discard changes?', undefined, [
        { text: 'Keep editing', style: 'cancel' },
        { text: 'Discard', style: 'destructive', onPress: () => navigation.goBack() },
      ])
    })
  }, [navigation])

  return <TextInput onChangeText={() => setIsDirty(true)} />
}

Dismiss Threshold

On Android and Web, dismissThreshold sets how far the user must drag the sheet down from its lowest detent to dismiss it.

export const App = () => {
  return (
    <TrueSheet
      dismissThreshold="short"
    >
      <View />
    </TrueSheet>
  )
}
ValueBehavior
'half'Dismisses past half of the lowest detent, or on a quick flick. Matches iOS. This is the default.
'short'Dismisses on a short drag or any downward fling, like Compose Material3's ModalBottomSheet.

On iOS, the system controls the threshold.

Dismissing Programmatically

MethodBehavior
dismissDismisses the sheet and all sheets presented on top of it.
dismissStackDismisses only the sheets on top, keeping this sheet presented.
TrueSheet.dismissAllDismisses every presented sheet.

All of them ignore dismissible. Listen for onWillDismiss and onDidDismiss to react to any dismissal.

Edit on GitHub

On this page