Bottom Sheets and Action Sheets
React Native ships no bottom sheet: what users expect on each platform, and which library or Modal gives it to you.
ActionSheetIOS.showActionSheetWithOptions(options: ActionSheetIOSOptions, callback: (buttonIndex: number) => void): voidA bottom sheet is a panel that rises from the bottom edge holding either a short list of actions or some secondary UI. React Native has no bottom sheet component. You choose a route, and the right one depends on how much the sheet has to do.
What each platform expects
On iOS a short list of choices is an action sheet. ActionSheetIOS.showActionSheetWithOptions(options, callback) takes an object whose options field is an array of button titles, plus cancelButtonIndex, destructiveButtonIndex, title and message; the callback receives the chosen index. It is iOS only. The module imports fine on Android and then does nothing, so branch on Platform.OS. Android's nearest built-in is Alert.alert with up to three buttons.
Choosing a route
If the sheet only needs to appear and disappear, a Modal with transparent and animationType="slide" is enough, and presentationStyle="pageSheet" gives the native card look on iOS. If you need dragging, snap points or a scrollable body, use @gorhom/bottom-sheet v5, which depends on react-native-reanimated and react-native-gesture-handler. Expo SDK 57 also ships @expo/ui, whose BottomSheet wraps the platform's own sheet, with a compatible API at @expo/ui/community/bottom-sheet. For one action-sheet call on both platforms, @expo/react-native-action-sheet gives a native sheet on iOS and its own on Android.
What trips people up
None of this is CSS. Height comes from flex layout and snap points, not from bottom: 0 and a transition.
Example
import { ActionSheetIOS, Alert, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
type PhotoActionsProps = {
onPick: (source: 'camera' | 'library') => void;
};
export default function PhotoActions({ onPick }: PhotoActionsProps) {
const show = () => {
if (Platform.OS === 'ios') {
// native sheet sliding up from the bottom edge; iOS only
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['Take Photo', 'Choose from Library', 'Cancel'],
cancelButtonIndex: 2,
title: 'Add a photo',
},
(buttonIndex) => {
if (buttonIndex === 0) onPick('camera');
if (buttonIndex === 1) onPick('library');
},
);
return;
}
// Android: no action sheet in core, so the system dialog stands in.
// Three buttons are placed neutral, negative, positive in array order.
Alert.alert('Add a photo', undefined, [
{ text: 'Take Photo', onPress: () => onPick('camera') },
{ text: 'Choose from Library', onPress: () => onPick('library') },
{ text: 'Cancel', style: 'cancel' },
]);
};
return (
<View style={styles.screen}>
<Pressable style={styles.button} onPress={show}>
<Text style={styles.buttonText}>Add a photo</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
button: { backgroundColor: '#1f6feb', borderRadius: 8, padding: 12 },
buttonText: { color: '#ffffff', textAlign: 'center' },
});When to use it
- Offer camera or photo library when the user adds an image.
- Show Delete, Rename and Cancel for a row in a list.
- Put filters or sort options in a panel the user can drag taller.
- Confirm a destructive choice with the sheet style each platform's users already recognise.
More examples
A draggable sheet with snap points
Pressing the button raises a sheet to 35% of the screen; dragging the handle up settles it at 80%, and dragging it down past the lower point closes it. This is the version to reach for when the sheet is part of the interface rather than a one-off question, since snap points, drag and a scrollable body are exactly what a Modal cannot give you. Install it with npx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler.
import { useRef } from 'react';
import { Button, StyleSheet, Text } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
export default function FilterSheet() {
const sheetRef = useRef<BottomSheet>(null);
return (
// this wrapper belongs at the root of the app; shown here for one file
<GestureHandlerRootView style={styles.root}>
<Button title="Filters" onPress={() => sheetRef.current?.snapToIndex(0)} />
<BottomSheet
ref={sheetRef}
index={-1} // -1 means closed on first render
snapPoints={['35%', '80%']} // the two heights it rests at
enablePanDownToClose
>
<BottomSheetView style={styles.content}>
<Text style={styles.heading}>Filters</Text>
<Text>Drag the handle to the taller snap point for all options.</Text>
</BottomSheetView>
</BottomSheet>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
content: { gap: 12, padding: 24 },
heading: { fontSize: 18, fontWeight: '600' },
});One action-sheet call for both platforms
One call site, a real UIAlertController sheet on iOS and a matching sheet drawn in JavaScript on Android, with Delete shown in red by destructiveButtonIndex. Use this when the only thing you want is a list of choices and the Platform.OS branch in the main example has started repeating itself. Note the callback index can be undefined, so test for the exact index rather than truthiness.
import { ActionSheetProvider, useActionSheet } from '@expo/react-native-action-sheet';
import { Button, StyleSheet, View } from 'react-native';
type RowActionsProps = {
onDelete: () => void;
onRename: () => void;
};
function RowActions({ onDelete, onRename }: RowActionsProps) {
const { showActionSheetWithOptions } = useActionSheet();
const open = () =>
showActionSheetWithOptions(
{
options: ['Delete', 'Rename', 'Cancel'],
destructiveButtonIndex: 0,
cancelButtonIndex: 2,
title: 'Item actions',
},
// undefined when the sheet is dismissed without a choice
(selectedIndex?: number) => {
if (selectedIndex === 0) onDelete();
if (selectedIndex === 1) onRename();
},
);
return <Button title="Actions" onPress={open} />;
}
export default function App() {
return (
// the provider renders the sheet above your tree, so it goes at the root
<ActionSheetProvider>
<View style={styles.screen}>
<RowActions onDelete={() => {}} onRename={() => {}} />
</View>
</ActionSheetProvider>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
});A sheet-style Modal, no extra packages
On iOS the panel slides up as a rounded card with the screen behind it still visible at the top; on Android it covers the screen, because presentationStyle is iOS only. This is the cheapest sheet you can ship: no Reanimated, no gesture handler, no drag. Take it when the sheet has one open state and one closed state and the user never resizes it.
import { useState } from 'react';
import { Button, Modal, Platform, StyleSheet, Text, View } from 'react-native';
export default function DetailsSheet() {
const [open, setOpen] = useState(false);
return (
<View style={styles.screen}>
<Button title="Details" onPress={() => setOpen(true)} />
<Modal
visible={open}
animationType="slide"
// iOS: a card that stops short of the top edge. No effect on Android.
presentationStyle={Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen'}
onRequestClose={() => setOpen(false)}
>
<View style={styles.sheet}>
<Text style={styles.heading}>Order details</Text>
<Text>Three items, delivered Thursday.</Text>
<Button title="Close" onPress={() => setOpen(false)} />
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 16 },
sheet: { flex: 1, gap: 16, padding: 24 },
heading: { fontSize: 20, fontWeight: '600' },
});
Discussion