The basic call, done properly
Three arguments: title, message, and an array of buttons. A fourth options object controls dismissal on Android. Here is a delete confirmation that is correct on both platforms:
import { Alert } from 'react-native';
function confirmDelete(onConfirm: () => void) {
Alert.alert(
'Delete workout?',
'This removes the workout and its 12 sets. You cannot undo this.',
[
// cancel: bold on iOS, negative slot on Android. Put it first.
{ text: 'Keep', style: 'cancel' },
// destructive: red on iOS, plain on Android.
{ text: 'Delete', style: 'destructive', onPress: onConfirm },
],
// Android only: block tap-outside and back-button dismissal so the
// user has to pick. onDismiss fires if they do dismiss (cancelable: true).
{ cancelable: false },
);
}Three details that matter. The title should be the question and the message the consequence, in that order, because iOS truncates long titles and Android bolds them. The cancel button is listed first so iOS places it correctly; iOS reorders cancel to the left of two buttons or the bottom of a stack regardless, so the array order is about Android, where buttons render in the order given. And cancelable: false is the only way to force a choice on Android; iOS alerts can never be dismissed by tapping outside.
Where iOS and Android disagree
| Behaviour | iOS | Android |
|---|---|---|
Max buttons | Unlimited (3+ stack vertically) | 3 (positive, negative, neutral) |
style: "cancel" | Bold text, moved to the left or bottom | Mapped to the negative slot |
style: "destructive" | Red text | No visual change |
Tap outside to dismiss | Never | Yes unless cancelable: false |
Back button dismisses | N/A | Yes unless cancelable: false |
Alert.prompt (text field) | Supported | No-op |
Dark mode | Follows system; userInterfaceStyle option | Follows app theme |
The three-button limit is the one that bites in production. Android’s native dialog has exactly three slots, so a fourth button is silently dropped. If a decision genuinely has four outcomes it is not an alert, it is an action sheet.
The destructive style is the one that disappoints. Designers expect the red confirm from iOS everywhere; Android simply does not have the concept in its dialog. Either accept the platform look, which is what most apps do, or move to a custom dialog for the two or three destructive flows where red matters.
A confirm() you can await
Alert.alert returns nothing and calls your buttons later, which makes it awkward inside an async save handler. Wrapping it in a Promise fixes that permanently:
// confirm.ts
import { Alert } from 'react-native';
type ConfirmOptions = { title: string; message?: string; confirmText?: string; destructive?: boolean };
export function confirm({ title, message, confirmText = 'OK', destructive = false }: ConfirmOptions) {
return new Promise<boolean>((resolve) => {
Alert.alert(
title,
message,
[
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
{ text: confirmText, style: destructive ? 'destructive' : 'default', onPress: () => resolve(true) },
],
// If Android dismisses via back button, resolve false instead of hanging forever.
{ cancelable: true, onDismiss: () => resolve(false) },
);
});
}
// usage
async function onDeleteAccount() {
const ok = await confirm({
title: 'Delete your account?',
message: 'All data is removed within 24 hours.',
confirmText: 'Delete',
destructive: true,
});
if (!ok) return;
await api.deleteAccount();
}The onDismiss line is the part people forget. Without it, an Android user who presses back leaves your Promise pending forever and the save handler never continues. On iOS it never fires because iOS alerts cannot be dismissed, which is fine.
Alert.prompt, and what to do on Android
Alert.prompt(title, message, callbackOrButtons, type, defaultValue, keyboardType) shows an iOS alert with a text field, with types of plain-text, secure-text, and login-password. It is useful for “name this playlist” moments. It does nothing on Android, and it does not throw, so a prompt that works perfectly in the iOS simulator ships broken to half your users.
The honest options: branch on Platform.OS and show a small Modal with a TextInput on Android, use react-native-dialog which ships a cross-platform prompt that mimics each platform, or skip the prompt entirely and make the name a field on the next screen. The third option is usually the best product decision.
When to build a custom dialog instead
Stay with Alert.alertas long as you can. It is accessible, it respects the user’s text size, it handles rotation and dark mode, and it is what users expect a system question to look like. Move to a custom dialog built on Modal when one of these is true:
- The dialog needs an image, an icon, a checkbox, or any content beyond two lines of text.
- The red destructive button must be red on Android too.
- The app ships to React Native Web, where Alert.alert is a no-op and shows nothing.
- You need a text field on both platforms.
- Brand consistency is a hard requirement, usually for a client who approved a design.
When you do build one, keep the native alert’s good habits: a title that is a question, buttons in the platform’s order, a dimmed backdrop that does not dismiss destructive confirmations, and accessibilityViewIsModal on the container so screen readers stay inside it.
Alert, toast, or sheet
The quick rule: an alert interrupts and demands an answer, a toast informs and goes away, a sheet offers choices. “Saved” is a toast. “Delete this?” is an alert. “Share via” is a sheet. Apps that use alerts for all three train users to tap through them without reading, which is exactly when the one destructive alert gets confirmed by accident.
That rule is also how apps generated with ShipNative decide: destructive and irreversible actions get the native Alert.alert with a cancel-first button order, successes get a toast, and anything with more than three choices gets a sheet. If you want a custom dialog for a specific flow, say so in the prompt and you get the Modal version with the same button semantics.