Modal
Modal draws its children in a native window above the whole app, with the props and back-button handling each platform needs.
<Modal visible={boolean} transparent animationType="fade" onRequestClose={() => {}}>{children}</Modal>A Modal puts content above everything else on screen. It exists because there is no CSS route to that result here: no position: fixed, no z-index stacking context, and a child cannot paint outside a parent that clips it. Modal steps out of the layout tree altogether by asking the platform for a window of its own.
How it works
It is a component you render, not a function you call. The visible prop decides whether it is on screen, so state drives it.
transparent is shorthand for transparent={true}; the two forms are the same thing. With it, the window has no background, so a half-opaque View of your own becomes the backdrop. Without it the window is filled with backdropColor, white by default. animationType accepts 'none' (the default), 'slide' or 'fade'. On iOS, presentationStyle takes 'fullScreen', 'pageSheet', 'formSheet' or 'overFullScreen', and only changes the result on larger devices.
What trips people up
onRequestClose is required on Android. It fires on the hardware back button, and if it does not set your state back to false, the modal never closes. On iOS it fires for the swipe-down dismissal.
Nesting one Modal inside another means a native window inside a native window, and the inner one may not appear. Render modals as siblings and let one piece of state decide which is open.
Example
import { useState } from 'react';
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
type ConfirmDeleteProps = {
label: string;
onConfirm: () => void;
};
export default function ConfirmDelete({ label, onConfirm }: ConfirmDeleteProps) {
const [open, setOpen] = useState(false);
return (
<View style={styles.screen}>
<Pressable style={styles.button} onPress={() => setOpen(true)}>
<Text style={styles.buttonText}>{label}</Text>
</Pressable>
<Modal
visible={open}
transparent
animationType="fade"
// required on Android: this is the hardware back button
onRequestClose={() => setOpen(false)}
>
<View style={styles.backdrop}>
<View style={styles.card}>
<Text style={styles.title}>Delete this item?</Text>
<Pressable onPress={() => setOpen(false)}>
<Text style={styles.action}>Cancel</Text>
</Pressable>
<Pressable onPress={() => { setOpen(false); onConfirm(); }}>
<Text style={styles.action}>Delete</Text>
</Pressable>
</View>
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
button: { backgroundColor: '#1f6feb', borderRadius: 8, padding: 12 },
buttonText: { color: '#ffffff', textAlign: 'center' },
// transparent={true} is what lets this tint show the app underneath
backdrop: { flex: 1, justifyContent: 'center', padding: 24, backgroundColor: 'rgba(0,0,0,0.5)' },
card: { backgroundColor: '#ffffff', borderRadius: 12, padding: 20, gap: 12 },
title: { fontSize: 17, fontWeight: '600' },
action: { color: '#1f6feb', fontSize: 16, paddingVertical: 8 },
});When to use it
- Confirm a destructive action with your own styled card rather than the system dialog.
- Show an image at full screen over a grid without fighting the parent view's clipping.
- Open a short form that must cover the current screen until it is saved or cancelled.
- Put a first-run explanation above the app on launch.
More examples
Several dialogs, one piece of state
Two dialogs sit side by side in the tree and a single union-typed value chooses between them, which is the fix for the nesting problem: on screen you see one card over a dimmed app, and switching from one button to the other never puts a window inside a window. Reach for this shape as soon as a screen has more than one dialog.
import { useState } from 'react';
import { Button, Modal, StyleSheet, Text, View } from 'react-native';
type Dialog = 'none' | 'share' | 'delete';
export default function ItemMenu() {
const [dialog, setDialog] = useState<Dialog>('none');
const close = () => setDialog('none');
return (
<View style={styles.screen}>
<Button title="Share" onPress={() => setDialog('share')} />
<Button title="Delete" onPress={() => setDialog('delete')} />
{/* siblings, never nested: one value decides which window opens */}
<Modal visible={dialog === 'share'} transparent animationType="fade" onRequestClose={close}>
<View style={styles.backdrop}>
<Text style={styles.card}>Share this item</Text>
<Button title="Close" onPress={close} />
</View>
</Modal>
<Modal visible={dialog === 'delete'} transparent animationType="fade" onRequestClose={close}>
<View style={styles.backdrop}>
<Text style={styles.card}>Delete this item</Text>
<Button title="Close" onPress={close} />
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', gap: 8, padding: 16 },
backdrop: { flex: 1, justifyContent: 'center', gap: 12, padding: 24, backgroundColor: 'rgba(0,0,0,0.5)' },
card: { backgroundColor: '#ffffff', borderRadius: 12, padding: 20, fontSize: 17 },
});A full-screen modal on Android
A reading view that covers the app: no transparency, so the window is opaque and slides up from the bottom. statusBarTranslucent is Android only, and because the content then starts behind the status bar you add the top padding yourself. onShow fires after the window appears, which is where work that should not run while it is still animating belongs.
import { useState } from 'react';
import { Button, Modal, Platform, StyleSheet, Text, View } from 'react-native';
export default function Reader() {
const [open, setOpen] = useState(false);
return (
<View style={styles.screen}>
<Button title="Read" onPress={() => setOpen(true)} />
<Modal
visible={open}
animationType="slide"
// Android only: let the window draw under the status bar
statusBarTranslucent
onShow={() => {
// runs once the window is on screen: start a timer, log a view
}}
onRequestClose={() => setOpen(false)}
>
<View style={[styles.page, { paddingTop: Platform.OS === 'android' ? 48 : 64 }]}>
<Text style={styles.body}>Article text</Text>
<Button title="Done" onPress={() => setOpen(false)} />
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
page: { flex: 1, gap: 16, paddingHorizontal: 20 },
body: { fontSize: 16, lineHeight: 24 },
});
Discussion