Alerts and Toasts
Alert.alert draws the platform's own dialog from a button array, and there is no built-in toast, so you branch or build one.
Alert.alert(title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions): voidAlert.alert opens the platform's native dialog. It is a function call, not a component, so it never appears in your render tree and you cannot style it. That is the trade: it looks exactly right on both platforms and you get no say in how it looks.
The signature
Alert.alert(title, message?, buttons?, options?), returning nothing. Each button is { text, onPress?, style?, isPreferred? }, where style is 'default', 'cancel' or 'destructive' and both style and isPreferred are iOS only. iOS accepts any number of buttons. Android accepts three at most and lays them out as neutral, negative, positive in array order. In options, cancelable and onDismiss are Android only and userInterfaceStyle is iOS only. Alert.prompt, which collects text, is iOS only.
Toasts
There is no cross-platform toast. ToastAndroid.show(message, duration) exists on Android, with ToastAndroid.SHORT and ToastAndroid.LONG; showWithGravity and showWithGravityAndOffset take TOP, BOTTOM or CENTER, but since Android 11 the system ignores gravity for text toasts. iOS has no equivalent. So either branch on Platform.OS or render your own banner, which also gets you one appearance on both platforms.
What trips people up
Alert.alert returns immediately. The lines after it run before the user has read a word.
Example
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
type DeleteRowProps = {
name: string;
onDelete: () => void;
};
export default function DeleteRow({ name, onDelete }: DeleteRowProps) {
const ask = () => {
Alert.alert(
'Delete note',
`"${name}" will be removed from this device.`,
[
// on Android this is the negative slot; on iOS it is greyed as Cancel
{ text: 'Cancel', style: 'cancel' },
// style and isPreferred are iOS only and ignored on Android
{ text: 'Delete', style: 'destructive', onPress: onDelete },
],
// Android only: a tap outside the dialog dismisses it with no choice
{ cancelable: true, onDismiss: () => {} },
);
// anything written here runs NOW, long before the user decides
};
return (
<View style={styles.row}>
<Text style={styles.name}>{name}</Text>
<Pressable onPress={ask} hitSlop={8}>
<Text style={styles.delete}>Delete</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row', // the default is column, so a row must say so
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
},
name: { fontSize: 16 },
delete: { color: '#c0392b', fontSize: 16 },
});When to use it
- Confirm deleting something before it goes.
- Tell the user a copy or save succeeded without interrupting them.
- Report a failed action in words, with a Retry button on the dialog.
- Ask for one short piece of text on iOS with Alert.prompt.
More examples
Alert wrapped in a promise
Alert's callback style forces the rest of your logic inside onPress, which gets awkward once two decisions follow each other. Wrapping it once gives you await confirm(...) everywhere else. The onDismiss resolve is the part people forget: on Android the dialog can close with no button pressed, and a promise that never settles hangs the function that awaited it.
import { Alert } from 'react-native';
export function confirm(title: string, message: string): Promise<boolean> {
return new Promise((resolve) => {
Alert.alert(
title,
message,
[
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
{ text: 'OK', onPress: () => resolve(true) },
],
// without this, an outside tap on Android leaves the promise pending forever
{ cancelable: true, onDismiss: () => resolve(false) },
);
});
}
// at the call site the dialog reads like any other async step
export async function deleteNote(id: string, remove: (id: string) => void) {
const ok = await confirm('Delete note?', 'This cannot be undone.');
if (!ok) return;
remove(id);
}
// Output: deleteNote resolves after the user taps; remove(id) runs only on OKA toast on Android, something else on iOS
ToastAndroid is a platform module, not a cross-platform API, so every call needs a guard: on iOS the import succeeds and the call does nothing useful. Use this when the message is genuinely disposable, such as Copied or Saved. Note the second function: positioning a toast stopped working on Android 11, so code that relies on gravity behaves differently across the devices you support.
import { Alert, Platform, ToastAndroid } from 'react-native';
export function notify(message: string) {
if (Platform.OS === 'android') {
// SHORT is roughly two seconds; LONG is roughly three and a half
ToastAndroid.show(message, ToastAndroid.SHORT);
return;
}
// iOS has no toast in core: a one-button alert is the quickest stand-in
Alert.alert(message);
}
export function notifyAtTop(message: string) {
if (Platform.OS !== 'android') return;
// gravity is ignored for text toasts from Android 11 onwards
ToastAndroid.showWithGravityAndOffset(
message,
ToastAndroid.LONG,
ToastAndroid.TOP,
0,
120,
);
}Your own banner, identical on both platforms
Tapping Save shows a dark pill near the bottom of the screen that disappears after 2.5 seconds. This is what most apps ship instead of toasts: one appearance everywhere, your own typography, and no platform branch. Two details to copy: the early return is inside the effect rather than wrapped around the hook, and the cleanup clears the timer so a second message does not get cut short by the first one's timeout.
import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
export default function BannerDemo() {
const [message, setMessage] = useState<string | null>(null);
// the hook runs on every render; the condition lives inside it, never around it
useEffect(() => {
if (message === null) return;
const timer = setTimeout(() => setMessage(null), 2500);
return () => clearTimeout(timer);
}, [message]);
return (
<View style={styles.screen}>
<Pressable onPress={() => setMessage('Saved')}>
<Text style={styles.link}>Save</Text>
</Pressable>
{message !== null && (
<View style={styles.banner}>
<Text style={styles.bannerText}>{message}</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
link: { color: '#1f6feb', fontSize: 16 },
banner: {
// absolute here means relative to this View, not to the window
position: 'absolute',
bottom: 32,
left: 16,
right: 16,
backgroundColor: '#222222',
borderRadius: 8,
padding: 12,
},
bannerText: { color: '#ffffff', textAlign: 'center' },
});
Discussion