Shipnative
ShipnativeShipnative
Sign in
GuideAugust 2026 · 8 min read

React Native Modal: Which One to Use (2026)

“Show a modal” sounds like one decision and is really four. React Native ships a Modal component, Expo Router presents whole screens modally, sheet libraries do the draggable kind, and Alertdoes the system dialog. Pick the wrong one and you get a screen with no back button, a keyboard covering an input, or the iOS bug where two stacked modals leave you looking at a blank white rectangle. Here’s how to choose, and how to wire the two you’ll actually use.

Four options, one question

OptionLooks likeAndroid backDeep-linkableUse it for
Expo Router modal routePlatform card / sheetWorks automaticallyYes — it has a URLCompose, filters, detail, settings — anything screen-shaped
Core <Modal> componentOverlay you style yourselfOnly via onRequestCloseNoConfirm dialogs, lightboxes, blocking loaders
Bottom sheet libraryDraggable sheet, snap pointsYou wire itNoHalf-open states, map details, players
Alert.alertTrue system dialogNative behaviourNoDestructive confirms — two lines of code

The question that decides it: could a user arrive here from a link or a notification?If yes — a post, an order, a compose screen — it’s a route, and presenting it modally is a styling choice on top of real navigation. If no — “delete this, are you sure?” — it’s an overlay, and making it a route just adds history entries the user has to back out of.

The good default: a modal route

In an Expo Router app, a modal is a normal screen with one extra prop in the layout. You get the platform presentation, swipe-to-dismiss on iOS, working hardware back on Android, and a URL — all without writing any of it:

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen
        name="compose"
        options={{
          presentation: 'modal',       // slides up, card on iOS
          title: 'New post',
        }}
      />
    </Stack>
  );
}
// app/compose.tsx
import { useState } from 'react';
import { Button, TextInput, View } from 'react-native';
import { router, useNavigation } from 'expo-router';

export default function ComposeScreen() {
  const [text, setText] = useState('');

  const close = () => {
    if (router.canGoBack()) router.back();
    else router.replace('/');     // opened via deep link: nothing to go back to
  };

  return (
    <View style={{ flex: 1, padding: 16, gap: 12 }}>
      <TextInput
        style={{ flex: 1, color: '#fff', fontSize: 16, textAlignVertical: 'top' }}
        placeholder="What's happening?"
        placeholderTextColor="#8b8b8b"
        multiline
        autoFocus
        value={text}
        onChangeText={setText}
      />
      <Button title="Post" onPress={async () => { await api.post(text); close(); }} />
    </View>
  );
}

The canGoBack() check is the part worth copying. A modal opened from a push notification or a shared link has no history behind it, so a bare router.back()leaves the user stuck on a screen they can’t leave. On iOS you can also set gestureEnabled: falsefor a screen with unsaved input, so a stray swipe can’t discard a half-written post — but if you do that, make the Cancel button obvious.

The overlay case: the core Modal component

For a confirm dialog or a lightbox — something that isn’t a place in the app — the built-in component is right. It renders above everything, and you style it entirely yourself:

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

export function ConfirmDialog({ visible, title, body, onConfirm, onCancel }) {
  return (
    <Modal
      visible={visible}
      transparent
      animationType="fade"
      onRequestClose={onCancel}     // Android hardware back — never omit this
      statusBarTranslucent
    >
      {/* tapping the scrim dismisses */}
      <Pressable style={styles.scrim} onPress={onCancel}>
        {/* stop the press from reaching the scrim */}
        <Pressable style={styles.card} onPress={(e) => e.stopPropagation()}>
          <Text style={styles.title}>{title}</Text>
          <Text style={styles.body}>{body}</Text>
          <View style={styles.actions}>
            <Pressable onPress={onCancel}><Text style={styles.cancel}>Cancel</Text></Pressable>
            <Pressable onPress={onConfirm}><Text style={styles.destructive}>Delete</Text></Pressable>
          </View>
        </Pressable>
      </Pressable>
    </Modal>
  );
}

const styles = StyleSheet.create({
  scrim: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', padding: 24 },
  card: { backgroundColor: '#16161a', borderRadius: 16, padding: 20, gap: 10 },
  title: { color: '#fff', fontSize: 18, fontWeight: '700' },
  body: { color: 'rgba(255,255,255,0.7)', fontSize: 15, lineHeight: 21 },
  actions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 20, marginTop: 8 },
  cancel: { color: 'rgba(255,255,255,0.7)', fontWeight: '600' },
  destructive: { color: '#ff5c5c', fontWeight: '600' },
});

Worth saying plainly: for exactly this dialog, Alert.alert() is two lines, uses the real system dialog, handles back and accessibility for free, and looks correct on both platforms without you maintaining it. Write the custom one only when the design genuinely needs to differ.

The traps

  1. No onRequestClose. Android’s back button does nothing and the user force-quits your app. It’s invisible during iOS development, which is exactly why it ships.
  2. Two core modals at once. Stacking iOS Modal instances is a long-standing source of blank screens and undismissable views. Close the first in the completion callback before opening the second, or move to navigation-based modals where the stack manages presentation.
  3. Keyboard avoidance placed outside. A modal is a separate view hierarchy — a KeyboardAvoidingView wrapping the screen behind it has no effect. Put it inside the modal content. The form validation guide covers the keyboard story in full.
  4. Modals for everything.Three modals deep and the user has no idea where they are or how to get back. If the content has its own content, it’s a screen.
  5. Ignoring the safe area. A full-screen modal draws under the notch and the home indicator. Use useSafeAreaInsets() inside the modal, not just on the screens beneath it.

If you find yourself adding a drag handle and snap points to a core Modal, stop — you’re rebuilding a bottom sheet, and that’s a solved problem.

The shortcut: describe the flow, not the presentation

Which modal to use is a five-second decision once you know the rule, and thirty minutes of the wrong behaviour once you don’t. The wiring — the route, the presentation option, the dismissal path, the back button — is identical in every app that has ever had a compose screen.

Describe the flow in ShipNative — “tapping New Post opens a compose screen that slides up, with Cancel and Post in the header” — and it sets up the Expo Router modal route, the dismissal handling and the safe-area padding, then runs it on your phone so you can test the swipe-down and the Android back button where they actually behave differently. Export the full Expo project whenever you want it.

Build it free

Describe your app in one sentence and have it running on your own phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

What is the best way to show a modal in React Native?

If the modal is a whole screen — a compose view, a filter panel, a detail you can navigate away from — use an Expo Router modal route. You get real navigation, a URL, working back behaviour, and the platform presentation for free. Reserve the core Modal component for small overlays that are genuinely not screens: a confirm dialog, a loading blocker, a photo lightbox.

Why does my React Native modal not close on Android back?

The core Modal component only calls onRequestClose on Android hardware back, and it is easy to leave that prop off because iOS works without it. Missing onRequestClose means the back button appears to do nothing — a documented Play Store review complaint. Always pass it, and point it at the same handler as your close button.

Can I open a modal from inside another modal?

On iOS, two overlapping instances of the core Modal component are a well-known source of blank screens and views that never dismiss. If you need a confirm dialog on top of a modal screen, use navigation-based modals so the stack owns the presentation, or close the first modal in the completion callback before opening the second.

How do I stop the keyboard covering an input inside a modal?

A modal is a separate view hierarchy, so a KeyboardAvoidingView outside it does nothing. Put the KeyboardAvoidingView inside the modal content, and use avoidKeyboard on iOS. If the modal is really a bottom sheet with a text field, use the sheet library components built for it rather than fighting the layout.

Can an AI app builder wire modals for me?

Yes. Describe the flow — "tapping New Post opens a compose screen that slides up, with Cancel and Post in the header" — and ShipNative sets up the Expo Router modal route, the presentation, and the dismissal behaviour, then previews it on your phone so you can check the swipe-down and the Android back button on real hardware.

→

React Native Bottom Sheet

When the modal needs to be draggable with a half-open state.

Read guide →
→

Expo Router vs React Navigation

Modal routes are a navigation feature — this is the layer underneath.

Read comparison →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.