React Native – Modals, Sheets and Overlays

July 4, 20263 min readUpdated 8/24/2026

A modal in React Native is a real native window, not a repositioned <View>. That single fact removes a whole class of web problems and introduces a few new ones.

No portal, and no z-index war

On the web, a dialog rendered where it sits in the tree gets clipped by any ancestor with overflow: hidden and buried under any ancestor that creates a stacking context. That is what createPortal exists to escape.

React Native's Modal renders outside the parent view hierarchy natively, so it is always on top. There is no createPortal in React Native and you will not need one. An overlay that is not a Modal — a toast, say — can simply be positioned absolutely and rendered last, because rendering order decides what is on top and nothing clips a sibling the way CSS does.

Building a bottom sheet

Bottom sheets are the native shape for what the web does with a centred dialog or a side drawer. They keep the primary action within thumb reach instead of at the top of the screen:

    <Modal
      visible={visible}
      transparent
      animationType="slide"
      onRequestClose={onClose}
      statusBarTranslucent
    >

transparent is what lets you draw your own backdrop — without it the modal is an opaque full-screen window. animationType="slide" gives the platform's own slide transition for free.

⚠️ onRequestClose is required on Android and ignored on iOS. It fires on the hardware back button. Omit it and back exits the entire app while a sheet is open — a bug you will never see if you only test on iOS.

The three things a web modal library gave you

React Native's Modal is deliberately low-level. Bootstrap or Radix handled these; here you wire them:

      <KeyboardAvoidingView
        style={styles.fill}
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      >
        <Pressable style={styles.backdrop} onPress={onClose} accessibilityLabel="Close" />

        <View
          style={[styles.panel, { paddingBottom: insets.bottom + theme.spacing.md }]}
          testID={testID}
        >

Tap-the-backdrop-to-close is a Pressable filling the screen behind the panel. The panel is deliberately not a child of it — nest them and every tap on the sheet's own content closes the sheet.

The keyboard needs KeyboardAvoidingView here as much as on a screen, with the same per-platform behavior.

The safe area applies again. A modal is its own window, so being inside a screen that already applied insets does not help it — it reads them itself. That paddingBottom is what keeps the sheet's buttons clear of the home indicator.

What is still missing

Focus is not trapped. A screen reader can walk out of the sheet into the screen behind it. React Native gives you accessibilityViewIsModal on iOS and importantForAccessibility="no-hide-descendants" on the content behind, and neither is automatic. The demo app documents this as a known gap rather than pretending otherwise — worth closing in a production app.

Making it dismissible

          <View style={styles.header}>
            <Text variant="heading">{title}</Text>
            <Pressable
              onPress={onClose}
              hitSlop={12}
              accessibilityRole="button"
              accessibilityLabel="Close"
              testID="sheet-close"
            >

An explicit close control matters more on a sheet than on a web dialog, because there is no Escape key. hitSlop={12} makes a small "✕" glyph a comfortable target without drawing a large button.

Closing before navigating

A trap worth naming, because the symptom is baffling. When a sheet contains a button that navigates elsewhere, close the sheet first:

  function goToCheckout() {
    onClose();
    router.push('/checkout');
  }

Navigate while the modal is still mounted and it stays over the new screen, invisible or not, swallowing every tap. The user arrives at a page that appears to be frozen. The demo app's test suite asserts the ordering rather than trusting it.

Toasts need none of this

    <View style={[styles.host, { top: insets.top + theme.spacing.sm }]} pointerEvents="none">
      {toasts.map((toast) => (
        <ToastCard key={toast.id} toast={toast} />
      ))}
    </View>

An absolutely positioned View, rendered last inside the provider. No Modal, no portal — being last in the tree is enough to be on top.

pointerEvents="none" is the important prop: it lets touches pass straight through the container to whatever is underneath, so a toast never blocks a button while it is visible.

Libraries

@gorhom/bottom-sheet gives you a draggable, snap-pointed sheet with proper gesture handling, and is what you want if the sheet is a primary interaction. The version above is a hundred lines and covers the common case — a panel that appears, takes a decision and goes away.

What is next

Animations and the Native Driver — one flag that decides whether an animation is smooth.