React Native Modals Done Right: Core Modal, Navigation Modals and Bottom Sheets (2026)

React Native modal guide for 0.87: core Modal, React Navigation modal and formSheet screens, @gorhom/bottom-sheet, and patterns that stop the usual bugs.

React Native Modals Done Right: Core Modal, Navigation Modals and Bottom Sheets (2026)

Every React Native app has at least one modal, and most of them are wrong in a small way. The confirm dialog that the Android back button cannot close. The bottom sheet that stutters because it animates on the JS thread. The full-screen editor that a user swiped away on iOS while your state still thinks it is open. None of these are hard to fix. They are just easy to get wrong because “react native modal” means three different things depending on who is talking.

This guide sorts them out. We cover the core Modal component in React Native 0.87, when to use a navigation-level modal screen instead, where bottom sheets fit, and the patterns that keep all of them working on both platforms. Code is against React Native 0.87.1, React Navigation 7.3, react-native-screens 4.27 and @gorhom/bottom-sheet 5.2.14, the current stable versions as of this writing.

Three kinds of react native modal

Before you pick a library, decide which of these you are building:

  • Overlay modal. A self-contained interaction that does not belong in navigation history: confirm dialogs, quick pickers, “rate this app” prompts. Use React Native’s built-in Modal.
  • Modal screen. A screen presented modally that is part of the navigation stack: a multi-step form, a compose screen, settings that deep-link. Use presentation: 'modal' (or formSheet) in a native stack.
  • Bottom sheet. A draggable panel with snap points that the user can peek at and expand: filters, share sheets, map details. Use @gorhom/bottom-sheet, or the native form sheet in react-native-screens if your needs are simple.

Most modal bugs come from using the first tool for the second job. A confirm dialog in the navigation stack pollutes back history; a checkout flow inside an overlay Modal cannot be deep-linked and fights the navigator for the back button.

The core Modal component

React Native’s Modal renders its children in a new native window above everything else in the app, including navigators. Minimal usage:

import { useState } from 'react';
import { Modal, View, Text, Pressable, StyleSheet } from 'react-native';

export function ConfirmDelete({ onConfirm }: { onConfirm: () => void }) {
  const [visible, setVisible] = useState(false);

  return (
    <>
      <Pressable onPress={() => setVisible(true)}>
        <Text>Delete</Text>
      </Pressable>

      <Modal
        visible={visible}
        transparent
        animationType="fade"
        onRequestClose={() => setVisible(false)}
      >
        <Pressable style={styles.backdrop} onPress={() => setVisible(false)}>
          <Pressable style={styles.card} onPress={() => {}}>
            <Text style={styles.title}>Delete this item?</Text>
            <View style={styles.row}>
              <Pressable onPress={() => setVisible(false)}><Text>Cancel</Text></Pressable>
              <Pressable onPress={() => { setVisible(false); onConfirm(); }}>
                <Text style={styles.danger}>Delete</Text>
              </Pressable>
            </View>
          </Pressable>
        </Pressable>
      </Modal>
    </>
  );
}

const styles = StyleSheet.create({
  backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24 },
  card: { backgroundColor: '#fff', borderRadius: 12, padding: 20 },
  title: { fontSize: 18, fontWeight: '600', marginBottom: 16 },
  row: { flexDirection: 'row', justifyContent: 'flex-end', gap: 24 },
  danger: { color: '#c00', fontWeight: '600' },
});

Three things in that snippet matter more than they look.

onRequestClose is not optional. The docs mark it required on Android and it is the only way the hardware back button (and the Apple TV menu button) closes your modal. While a Modal is open, BackHandler events are swallowed, so if you forget this prop the back button does nothing and Android users assume the app is frozen. Wire it to the same setter as your Cancel button.

The nested Pressable with an empty onPress stops taps inside the card from bubbling to the backdrop and closing the dialog. It is a common trick and it works, but if the card contains scrollable content, prefer a View with onStartShouldSetResponder={() => true} so you do not steal touches from children.

transparent plus animationType=”fade” gives you the dimmed-backdrop dialog look on both platforms. animationType="slide" comes from the bottom and reads as a sheet. none is for cases where you animate the contents yourself.

Props that matter in 0.87

The full list is in the Modal reference. The ones that actually change behavior:

  • presentationStyle (iOS): fullScreen, pageSheet, formSheet or overFullScreen. pageSheet gives the native card-style sheet with a swipe-down dismiss on iPhone. When you use it, onRequestClose fires on that swipe, so it doubles as your dismiss handler on iOS.
  • allowSwipeDismissal (iOS): lets a full-screen modal be swiped down. Requires onRequestClose.
  • backdropColor: background of the modal container when transparent is false. Defaults to white, which is the reason your dark-mode app flashes white when a modal opens. Set it.
  • statusBarTranslucent and navigationBarTranslucent (Android): draw under the system bars. With edge-to-edge now the default on Android 15+ targets, you want both set to true for full-screen overlays or your backdrop stops short of the top of the screen.
  • onShow and onDismiss: lifecycle callbacks. onDismiss is iOS only; on Android, run your cleanup in the same place you flip visible.
  • supportedOrientations (iOS): ignored under pageSheet and formSheet, and still bounded by your Info.plist.

Where the core Modal falls down

It has no built-in backdrop press handling, no snap points, no gesture-driven dismiss on Android, and its animations run through the old Animated bridge rather than Reanimated. Multiple stacked modals behave differently per platform. And because it creates a new native window, anything rendered inside it sits outside your navigator, which means useNavigation works but modal content does not appear in navigation state, deep links or the back stack. That is fine for dialogs. It is the wrong shape for screens.

Modal screens with React Navigation

If the modal is a screen, make it a screen. With @react-navigation/native-stack you set presentation on the route and the navigator presents it with the platform’s native modal transition, back gesture and history handling:

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="Compose"
        component={ComposeScreen}
        options={{ presentation: 'modal', headerShown: true, title: 'New message' }}
      />
      <Stack.Screen
        name="Filters"
        component={FiltersScreen}
        options={{
          presentation: 'formSheet',
          sheetAllowedDetents: [0.4, 0.9],
          sheetGrabberVisible: true,
          sheetCornerRadius: 20,
        }}
      />
    </Stack.Navigator>
  );
}

Then navigation.navigate('Compose') opens it and navigation.goBack() closes it. The back button, the iOS swipe-down, deep links and state restoration all work with zero extra code. That is the entire argument for this approach.

The presentation values worth knowing, per the native stack docs:

  • modal: the standard modal. On iOS this is the card-style sheet that stacks over the parent; on Android it slides up.
  • fullScreenModal: covers everything, cannot be swiped away on iOS. Use it for flows that must complete or be cancelled explicitly.
  • transparentModal: keeps the previous screen visible underneath. This is how you build a custom dialog that still lives in navigation history.
  • formSheet: a native bottom sheet. Uses UIModalPresentationFormSheet on iOS and BottomSheetBehavior on Android, with detents, a grabber, corner radius and dimming controlled through the sheet* options. Android is limited to three detents.

Using Expo Router? Same options, different syntax: a modal.tsx file in your app directory plus <Stack.Screen name="modal" options={{ presentation: 'modal' }} /> in the layout. The Expo Router modals guide also covers the web case, where a modal is just another route and you have to provide the dismiss link yourself with router.canGoBack().

A note on the header inside a modal screen

Modal screens keep their own header, so give them a close button. On iOS users expect “Cancel” or “Done” text on the left or right; on Android an X icon on the left. A small headerLeft that calls navigation.goBack() is usually enough, and it saves you from the “my modal has no way out on iPad” bug report.

Bottom sheets: native formSheet or @gorhom/bottom-sheet

Two years ago the answer to “bottom sheet in React Native” was always @gorhom/bottom-sheet. Today there are two good answers.

Native formSheet (react-native-screens 4.x via React Navigation) is the right default when the sheet is a screen: a filters screen, a detail card from a map, an item editor. You get real platform sheets, correct keyboard behavior and accessibility for free, and the sheet is in navigation state. Limits: three detents on Android, less control over the drag animation, and content-driven sizing ('fitToContents') has edge cases with dynamic content. Also note the docs’ warning that sheetExpandsWhenScrolledToEdge only works if the ScrollView is the first-child chain from the screen root.

@gorhom/bottom-sheet 5.2.14 is the right tool when the sheet is a component inside a screen: it stays mounted, it has multiple snap points with fully custom animation (Reanimated 3.16+ or 4.x plus Gesture Handler 2.16+), it supports FlashList and dynamic sizing, and it works on React Native Web. Basic usage:

import { useRef, useMemo, useCallback } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';

export function FilterSheet() {
  const ref = useRef<BottomSheet>(null);
  const snapPoints = useMemo(() => ['35%', '85%'], []);

  const renderBackdrop = useCallback(
    (props: any) => <BottomSheetBackdrop {...props} appearsOnIndex={0} disappearsOnIndex={-1} />,
    []
  );

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* your screen */}
      <BottomSheet
        ref={ref}
        index={-1}
        snapPoints={snapPoints}
        enablePanDownToClose
        backdropComponent={renderBackdrop}
      >
        <BottomSheetView style={{ padding: 16 }}>
          {/* filters */}
        </BottomSheetView>
      </BottomSheet>
    </GestureHandlerRootView>
  );
}

Open it with ref.current?.snapToIndex(0) and close with ref.current?.close(). If the sheet contains a text input, use BottomSheetTextInput and the keyboard-aware keyboardBehavior props, otherwise the keyboard will cover your input on Android.

What about react-native-modal?

You will still see react-native-modal in every tutorial from 2019 to 2023. Be careful. The last stable release, 13.0.1, shipped in March 2022; a 14.0.0 release candidate appeared in March 2025 and has not moved since. It wraps the core Modal and adds backdrop press handling, swipe-to-dismiss and animations through react-native-animatable. Everything it does is now covered by either presentationStyle="pageSheet" with onRequestClose, a transparentModal navigation screen, or a bottom sheet. For new code in 2026, skip it.

If you want a managed, imperative modal API (“open this modal from anywhere, await the result”), two maintained options exist: react-native-modalfy (3.7.3, updated August 2026) and react-native-magic-modal (10.2.1, August 2026), which builds on Reanimated and Screens. Both are good for apps with dozens of modals that need a central registry. Overkill for most apps.

Patterns that prevent the usual bugs

Own the visibility state above the modal

Keep visible in the parent or in a store, never inside the modal component. Modals close from many directions (button, backdrop, back gesture, swipe, deep link) and every one of them must end at the same setter. If two pieces of state disagree about whether the modal is open, iOS will happily show a dismissed modal that your app still thinks is presented, and the next visible={true} does nothing.

Wait for onDismiss before presenting the next one

On iOS, presenting a second modal while the first is still animating out throws “Attempt to present while a presentation is in progress” and the second one silently never shows. Chain them through onDismiss, or use a navigation stack where the navigator serializes transitions for you.

Safe areas inside modals

A core Modal is a new window, so your app’s SafeAreaProvider insets do not automatically reach it. Wrap modal content in its own SafeAreaView from react-native-safe-area-context, or, for pageSheet on iOS, rely on the system inset the sheet already applies.

Accessibility

Give the modal container accessibilityViewIsModal on iOS so VoiceOver focus stays inside, label the close control (“Close”, not just an icon), and move focus to the modal’s title on open. For a dialog, set accessibilityRole="alert" on the message so screen readers announce it. Native formSheet and navigation modals get most of this from the platform; the core Modal gets none of it without your help.

Do not animate content on the JS thread

If your sheet drags at 40 fps while a list is loading, the gesture is running through the bridge. @gorhom/bottom-sheet and the native form sheet both run on the UI thread. If you hand-roll a sheet with Animated, at least use useNativeDriver: true, and honestly, in 2026, reach for Reanimated instead.

Which one should you use?

Need Use
Confirm, alert, quick picker Core Modal (transparent, fade)
Compose, form, settings flow Native stack presentation: 'modal'
Must-complete flow (checkout, onboarding) fullScreenModal
Custom dialog that lives in history transparentModal
Bottom sheet as a screen formSheet with sheetAllowedDetents
Bottom sheet as a component, custom animation, web @gorhom/bottom-sheet
Dozens of modals, imperative API react-native-modalfy or react-native-magic-modal

Component libraries also ship their own dialogs: if you already use React Native Paper, its Dialog and Portal cover the confirm case with Material styling and correct focus handling. For a chat app, the compose-message and attachment-picker modals are exactly the two shapes above (a navigation modal and a bottom sheet), and we walk through both in our chat app tutorial. If you are on Expo and wondering how Expo Router fits, our Expo guide covers the routing layer.

FAQ

How do I close a React Native modal with the Android back button?
Pass onRequestClose to the core Modal and set your visible state to false inside it. Navigation modals handle the back button automatically.

Is react-native-modal still maintained?
Effectively no. The last stable release was 13.0.1 in March 2022 and 14.0.0 has been a release candidate since March 2025. Use the core Modal, a navigation modal, or a bottom sheet instead.

How do I make a modal transparent in React Native?
Set transparent to true on the core Modal and style your own backdrop, or use presentation: 'transparentModal' for a navigation screen.

Can I use a bottom sheet without @gorhom/bottom-sheet?
Yes. React Navigation’s native stack supports presentation: 'formSheet' with native detents on both platforms through react-native-screens 4.x.

Why does my modal flash white in dark mode?
The core Modal’s backdropColor defaults to white when transparent is false. Set it to your dark background color, or use transparent and draw the backdrop yourself.

Leave a Reply 0

Your email address will not be published. Required fields are marked *