How to make a modal dialog in React Native
By the LumioUI team · Updated
A modal is a panel that sits above the screen and asks for one decision, over a dimmed backdrop. In React Native, render React Native's Modal with transparent and no built-in animation, animate the backdrop and panel yourself with Reanimated, and keep it mounted until the exit animation finishes. The LumioUI Dialog does this: the backdrop darkens first, the panel rises, and on close it fades out in place.
Live demo of the LumioUI Dialog component. It's interactive, so try it.
How a modal works
- Above everything. React Native's
Modalrenders in its own window, over headers and tab bars. Settransparentso your backdrop shows, andstatusBarTranslucenton Android so it covers the status bar. - Two layers. The backdrop fades; the panel scales or rises. Animate them separately so the backdrop leads in and the panel leads out.
- Exit animations need a delay.
Modaldrops its content as soon asvisibleis false. Keep amountedstate that lags behindopenby the exit duration. - Ways to close. A tap on the backdrop, the Android back button (
onRequestClose) and a Cancel button should all close it. - Accessibility.
accessibilityViewIsModalkeeps VoiceOver inside the panel; give the title theheaderrole and label the backdrop "Close".
import * as React from "react";
import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } from "react-native-reanimated";
export function ConfirmModal({ open, onClose, onConfirm }) {
// RN's Modal removes its content the moment `visible` turns false, so
// exit animations never run. Keep it mounted until the panel has left.
const [mounted, setMounted] = React.useState(open);
React.useEffect(() => {
if (open) setMounted(true);
else if (mounted) {
const t = setTimeout(() => setMounted(false), 160);
return () => clearTimeout(t);
}
}, [open]);
return (
<Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={onClose}>
{open && (
<View style={styles.root}>
<Animated.View entering={FadeIn.duration(160)} exiting={FadeOut.duration(120)} style={styles.backdrop}>
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} accessibilityLabel="Close" />
</Animated.View>
<Animated.View entering={ZoomIn.duration(200)} exiting={ZoomOut.duration(120)}
style={styles.panel} accessibilityViewIsModal>
<Text accessibilityRole="header" style={styles.title}>Delete this photo?</Text>
<Text>It will be removed from all your albums.</Text>
<View style={styles.row}>
<Pressable onPress={onClose} accessibilityRole="button"><Text>Cancel</Text></Pressable>
<Pressable onPress={onConfirm} accessibilityRole="button"><Text>Delete</Text></Pressable>
</View>
</Animated.View>
</View>
)}
</Modal>
);
}
const styles = StyleSheet.create({
root: { flex: 1, justifyContent: "center", padding: 24 },
backdrop: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, backgroundColor: "rgba(0,0,0,0.4)" },
panel: { backgroundColor: "#fff", borderRadius: 20, padding: 20, gap: 8 },
title: { fontSize: 17, fontWeight: "600" },
row: { flexDirection: "row", justifyContent: "flex-end", gap: 16, marginTop: 12 },
}); For a choice from a list rather than a yes or no, a sheet from the bottom usually reads better on phones: see how to make a bottom sheet.
The quick way: LumioUI Dialog
The Dialog handles mounting, motion, reduced motion, haptics, back-button and backdrop dismissal, and screen readers. It's free and runs in Expo Go.
import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from "@/components/ui/dialog/dialog";
export function DeletePhoto({ onDelete }) {
const [open, setOpen] = React.useState(false);
return (
<>
<Button variant="destructive" onPress={() => setOpen(true)}>Delete photo</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete this photo?</DialogTitle>
<DialogDescription>It will be removed from all your albums.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose><Button variant="outline">Cancel</Button></DialogClose>
{/* DialogClose runs the button's onPress, then closes */}
<DialogClose><Button variant="destructive" onPress={onDelete}>Delete</Button></DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
} openandonOpenChangecontrol it; or usedefaultOpenand let it manage itself.DialogClosewraps any button: it runs the button's ownonPress, then closes.DialogContent material="glass"switches to liquid glass for dialogs over photos or video.- For a modal with several steps that resizes to fit each one, see Morphing Modal (Pro).
Add it to your app
LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:
npx @lumioui/cli init
npx @lumioui/cli add dialog It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Dialog API.
Related components
- Action Sheet
- Bottom Sheet (Pro)
- Popover
- Morphing Modal (Pro)
More React Native guides
- How to make a bottom sheet in React Native
- Animated tabs with a sliding indicator in React Native
- How to show toast notifications in React Native
- Skeleton loading screens in React Native
- How to add dark mode and a theme toggle in React Native
- How to build an accordion in React Native
- How to make a side drawer menu in React Native
- How to make an animated heart like button in React Native
- How to build an animated carousel in React Native
- How to make an animated splash screen in React Native and Expo
- How to build a month calendar in React Native