TL;DR
Bottom sheets are one of the most-used mobile UI patterns — filter panels, share menus, detail views — and the React Native ecosystem has multiple solid options. Gorhom Bottom Sheet v5 is the most feature-complete: Reanimated-powered smooth animations, gesture-controlled dragging, snap points, dynamic content sizing, and a BottomSheetFlatList/BottomSheetScrollView that properly handles scrollable content inside sheets. Expo Bottom Sheet is a wrapper around Gorhom built for the Expo/expo-ui ecosystem — simplifies the API for common use cases and integrates with the Expo design system. React Native Simple Bottom Sheet (RNSBS) is the lightweight option — minimal dependencies, simple snap-to-open/close behavior, and easy integration when you just need a basic sheet without the full Gorhom power. For complex sheets with scrollable content, multiple snap points, and gestures: Gorhom. For Expo-native apps wanting a simpler API: Expo Bottom Sheet. For simple, minimal-dependency sheets: RNSBS.
Key Takeaways
- Gorhom v5 uses Reanimated 3 — worklet-based animations run on UI thread, not JS thread
- Gorhom supports dynamic sizing —
enableDynamicSizingmeasures content height automatically - BottomSheetFlatList — scroll-inside-sheet with proper gesture discrimination
- Snap points —
["25%", "50%", "90%"]or pixel values[300, 600] - Backdrop component — dimmed overlay that dismisses sheet on tap
- Expo Bottom Sheet uses Gorhom under the hood — same performance characteristics
- RNSBS has no Reanimated dependency — uses React Native
AnimatedAPI
Use Case Guide
Scrollable list inside sheet → Gorhom BottomSheetFlatList
Dynamic height (auto-size) → Gorhom enableDynamicSizing
Multiple snap positions → Gorhom snapPoints prop
Simple open/close sheet → RNSBS or Expo Bottom Sheet
Expo project (managed workflow) → Expo Bottom Sheet
Filter panel with form → Gorhom BottomSheetScrollView
Confirmation / action sheet → Any (RNSBS simplest)
Full-screen drawer behavior → Gorhom at snapPoints={["100%"]}
Gorhom Bottom Sheet v5
The industry standard for React Native bottom sheets — smooth Reanimated animations, gesture-based dragging, and first-class support for scrollable content inside the sheet.
Installation
npm install @gorhom/bottom-sheet@5
# Required dependencies:
npm install react-native-reanimated react-native-gesture-handler
npx pod-install # iOS
Gesture Handler Setup
// app/_layout.tsx or App.tsx
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* rest of app */}
</GestureHandlerRootView>
);
}
Basic Bottom Sheet
import { useRef, useCallback } from "react";
import { View, Text, Button, StyleSheet } from "react-native";
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
export function FilterPanel() {
const bottomSheetRef = useRef<BottomSheet>(null);
// Snap to 50% on open, dismiss on close
const snapPoints = ["50%", "90%"];
const handleOpen = useCallback(() => {
bottomSheetRef.current?.expand();
}, []);
const handleClose = useCallback(() => {
bottomSheetRef.current?.close();
}, []);
const handleSheetChanges = useCallback((index: number) => {
console.log("Sheet position changed to index:", index);
}, []);
return (
<View style={styles.container}>
<Button title="Open Filters" onPress={handleOpen} />
<BottomSheet
ref={bottomSheetRef}
index={-1} // -1 = closed (hidden)
snapPoints={snapPoints}
onChange={handleSheetChanges}
enablePanDownToClose={true}
>
<BottomSheetView style={styles.contentContainer}>
<Text style={styles.title}>Filters</Text>
<Button title="Apply" onPress={handleClose} />
</BottomSheetView>
</BottomSheet>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
contentContainer: { flex: 1, padding: 20, alignItems: "center" },
title: { fontSize: 18, fontWeight: "bold", marginBottom: 16 },
});
Backdrop (Dimmed Overlay)
import BottomSheet, { BottomSheetBackdrop, BottomSheetView } from "@gorhom/bottom-sheet";
import { useCallback } from "react";
export function BottomSheetWithBackdrop() {
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
pressBehavior="close" // Tap backdrop to close
opacity={0.5}
/>
),
[]
);
return (
<BottomSheet
index={-1}
snapPoints={["50%"]}
backdropComponent={renderBackdrop}
enablePanDownToClose
>
<BottomSheetView style={{ padding: 20 }}>
<Text>Sheet content</Text>
</BottomSheetView>
</BottomSheet>
);
}
Scrollable Content (BottomSheetFlatList)
import BottomSheet, { BottomSheetFlatList } from "@gorhom/bottom-sheet";
interface Product {
id: string;
name: string;
price: number;
}
export function ProductSheet({ products }: { products: Product[] }) {
const renderItem = useCallback(
({ item }: { item: Product }) => (
<View style={styles.item}>
<Text>{item.name}</Text>
<Text>${item.price}</Text>
</View>
),
[]
);
return (
<BottomSheet
index={0}
snapPoints={["50%", "90%"]}
enablePanDownToClose
>
{/* BottomSheetFlatList handles gesture discrimination
between sheet drag and list scroll automatically */}
<BottomSheetFlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
/>
</BottomSheet>
);
}
Dynamic Height (Auto-Size Content)
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
export function AutoSizedSheet() {
return (
<BottomSheet
index={0}
enableDynamicSizing={true} // Height matches content
enablePanDownToClose
>
<BottomSheetView>
{/* Sheet height adjusts to fit this content */}
<Text style={{ padding: 20, fontSize: 16 }}>
This sheet auto-sizes to fit its content.
</Text>
<View style={{ padding: 20 }}>
<Button title="Action 1" onPress={() => {}} />
<Button title="Action 2" onPress={() => {}} />
<Button title="Cancel" onPress={() => {}} />
</View>
</BottomSheetView>
</BottomSheet>
);
}
BottomSheetModal (Global Modal System)
import { useCallback, useRef } from "react";
import {
BottomSheetModal,
BottomSheetModalProvider,
BottomSheetView,
} from "@gorhom/bottom-sheet";
// Wrap app with BottomSheetModalProvider
export function AppWithModalProvider() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<App />
</BottomSheetModalProvider>
</GestureHandlerRootView>
);
}
// Use the modal from any component
function ProductCard({ product }: { product: Product }) {
const modalRef = useRef<BottomSheetModal>(null);
const handlePresent = useCallback(() => {
modalRef.current?.present();
}, []);
const handleDismiss = useCallback(() => {
modalRef.current?.dismiss();
}, []);
return (
<View>
<TouchableOpacity onPress={handlePresent}>
<Text>{product.name}</Text>
</TouchableOpacity>
<BottomSheetModal
ref={modalRef}
snapPoints={["60%"]}
enablePanDownToClose
>
<BottomSheetView style={{ padding: 20 }}>
<Text style={{ fontSize: 20, fontWeight: "bold" }}>{product.name}</Text>
<Text>Price: ${product.price}</Text>
<Button title="Add to Cart" onPress={handleDismiss} />
</BottomSheetView>
</BottomSheetModal>
</View>
);
}
Expo Bottom Sheet
Expo's bottom sheet component from the expo-ui ecosystem — a simpler API wrapping Gorhom for common Expo use cases.
Installation
npx expo install expo-bottom-sheet
Usage
import { BottomSheet, BottomSheetView } from "expo-bottom-sheet";
import { useState } from "react";
export function SimpleSheet() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button title="Open" onPress={() => setIsOpen(true)} />
<BottomSheet
isOpen={isOpen}
onClose={() => setIsOpen(false)}
>
<BottomSheetView>
<Text style={{ padding: 20 }}>Sheet content here</Text>
<Button title="Close" onPress={() => setIsOpen(false)} />
</BottomSheetView>
</BottomSheet>
</>
);
}
React Native Simple Bottom Sheet (RNSBS)
A lightweight bottom sheet with no Reanimated dependency — uses React Native's built-in Animated API for a simple open/close behavior.
Installation
npm install rn-simple-bottom-sheet
Usage
import RNSimpleBottomSheet from "rn-simple-bottom-sheet";
import { useRef } from "react";
export function SimpleSheet() {
const sheetRef = useRef<RNSimpleBottomSheet>(null);
return (
<View style={{ flex: 1 }}>
<Button title="Open" onPress={() => sheetRef.current?.open()} />
<RNSimpleBottomSheet
ref={sheetRef}
height={300}
draggable={true}
onClose={() => console.log("Sheet closed")}
>
<View style={{ padding: 20 }}>
<Text>Simple sheet content</Text>
<Button title="Close" onPress={() => sheetRef.current?.close()} />
</View>
</RNSimpleBottomSheet>
</View>
);
}
Feature Comparison
| Feature | Gorhom v5 | Expo Bottom Sheet | RNSBS |
|---|---|---|---|
| Animation engine | Reanimated 3 (UI thread) | Reanimated 3 | RN Animated (JS thread) |
| Snap points | ✅ Multiple | ✅ | ❌ (height only) |
| Dynamic sizing | ✅ enableDynamicSizing | ✅ | ❌ |
| Scrollable content | ✅ BottomSheetFlatList | ✅ | ⚠️ Manual |
| Backdrop | ✅ BottomSheetBackdrop | ✅ | ✅ |
| BottomSheetModal | ✅ (global modal system) | ✅ | ❌ |
| Gesture pan-to-dismiss | ✅ | ✅ | ✅ |
| Expo Go compatible | ❌ (Reanimated) | ✅ | ✅ |
| Setup complexity | Medium (GestureHandler) | Low | Low |
| Bundle size | Large | Medium | Small |
| TypeScript | ✅ Excellent | ✅ | ✅ |
| GitHub stars | 7.5k | N/A (new) | 0.5k |
| npm weekly | 700k | Growing | 20k |
When to Use Each
Choose Gorhom Bottom Sheet if:
- Scrollable list/FlatList inside the sheet (proper gesture discrimination)
- Multiple snap points (25%, 50%, 90%) for expanding/collapsing
enableDynamicSizingto fit variable-height contentBottomSheetModalfor a centralized modal system- Full control over backdrop, handle, and animation
Choose Expo Bottom Sheet if:
- Building with Expo SDK and want API consistency with
expo-uicomponents - Standard open/close behavior is sufficient — no complex snap points needed
- Prefer Expo's managed approach to native modules
Choose RNSBS if:
- Zero tolerance for Reanimated/Gesture Handler peer dependencies
- Works in Expo Go (no bare workflow needed)
- Simple open/close action sheet behavior — no scrollable content
- Lightweight dependencies are a priority
Gesture Handler and Reanimated: Why Setup Matters
The GestureHandlerRootView wrapper and Reanimated configuration aren't boilerplate — they explain why Gorhom's sheets feel fluid while pure JS-based solutions feel janky.
React Native's JavaScript thread runs your app logic: state updates, API calls, rendering decisions. On a loaded device, this thread is frequently busy, and any animation driven by it drops frames whenever JS is congested. React Native Reanimated moves animation computations to the UI thread — the same native thread the OS uses for rendering. This is why Gorhom sheets stay smooth even when a list inside the sheet is loading data or the app is processing a heavy computation in the background.
The GestureHandlerRootView wrapper is equally important. React Native's built-in touch system (the "PanResponder" API) doesn't compose well when multiple gesture recognizers compete — a FlatList inside a sheet and the sheet's own drag gesture would fight over touch events. React Native Gesture Handler uses native gesture recognizers on iOS (UIGestureRecognizer) and Android (GestureDetector), which compose correctly via priority and activation rules. This is why BottomSheetFlatList can distinguish between "user is scrolling the list" and "user is dragging the sheet down" — it's not heuristic guessing, it's native gesture composition.
RNSBS avoids these dependencies by using Animated (JS-thread) and React Native's built-in PanResponder. The tradeoff is real: sheets are fine for simple open/close, but scrollable content inside RNSBS sheets requires careful gesture management that you handle manually.
Keyboard-Aware Bottom Sheets
Forms inside bottom sheets create a common problem on both iOS and Android: the software keyboard obscures the focused input field. The standard KeyboardAvoidingView solution doesn't compose well with bottom sheets because the sheet itself is absolutely positioned — KeyboardAvoidingView's offset calculations assume normal document flow.
Gorhom Bottom Sheet v5 solves this with the keyboardBehavior prop. Setting keyboardBehavior="extend" causes the sheet to expand upward as the keyboard rises, keeping the form content visible. keyboardBehavior="interactive" creates a seamless push — the sheet moves up with the keyboard in real-time as it animates in, rather than jumping. On Android, you also need android_keyboardInputMode="adjustResize" in the activity manifest (or via expo-build-properties config plugin) to make the keyboard intrusion measurable by the native layout system.
For Expo Bottom Sheet, the keyboard behavior inherits from the Gorhom dependency — the same props are available but may require passing through bottomSheetProps. With RNSBS, keyboard handling is manual: you'd wrap the sheet content in KeyboardAvoidingView yourself and tune the keyboardVerticalOffset based on the sheet's current position, which changes with snap points.
Migrating from Gorhom v4 to v5
Gorhom v5 is not a drop-in replacement for v4. The API changed significantly to align with Reanimated 3 and the newer Gesture Handler APIs.
The most impactful change: snapPoints no longer accepts string[] as the sole format — percentage strings like "50%" still work, but the internal representation changed. If you were using SnapPointsValue TypeScript types from v4, update the import paths.
BottomSheetScrollView is still present but BottomSheetView is now the preferred wrapper for non-scrolling content — using raw View inside a sheet caused layout measurement bugs in v4 that confused the snap calculation. In v5, always wrap content in BottomSheetView or BottomSheetScrollView.
The enableContentPanningGesture prop was removed — content panning is always enabled in v5 when using BottomSheetFlatList or BottomSheetScrollView. If you were explicitly disabling it, remove the prop; the gesture system now handles discrimination automatically.
Check the v5 migration guide at gorhom.github.io before upgrading: several prop renames and package version requirements (minimum Reanimated 3.0, minimum Gesture Handler 2.0) need to be satisfied before the types will compile cleanly.
Accessibility Considerations
Bottom sheets present accessibility challenges that are easy to overlook during development but become blockers for users relying on screen readers.
On iOS, VoiceOver needs to know the sheet is a modal context. Gorhom handles this via accessible and accessibilityLabel on the BottomSheet component, and the backdrop tap-to-dismiss is natively accessible — VoiceOver users can double-tap anywhere outside the sheet to close it. Ensure your sheet's first focusable element has a clear accessibilityLabel so VoiceOver announces the context change when the sheet opens.
On Android, TalkBack focus should move into the sheet when it opens. This requires calling AccessibilityInfo.setAccessibilityFocus on the sheet's first interactive element after the onChange callback fires at index: 0. Without this, TalkBack users remain focused behind the sheet overlay and interact with the wrong elements.
For BottomSheetFlatList, add accessibilityLabel to list items and accessibilityRole="list" to the container. The gesture-based dismiss (pan down) should be supplemented with a visible "close" button — screen reader users cannot perform arbitrary pan gestures.
Methodology
Data sourced from Gorhom Bottom Sheet documentation (gorhom.github.io/react-native-bottom-sheet), Expo Bottom Sheet documentation (docs.expo.dev), React Native Simple Bottom Sheet GitHub repository, npm download statistics as of February 2026, GitHub star counts as of February 2026, and community discussions from the React Native Discord and r/reactnative.
Related: FlashList vs FlatList vs LegendList for the high-performance list components used inside BottomSheetFlatList, or React Native Reanimated vs Moti vs Skia for the animation library that powers Gorhom's smooth sheet transitions.
See also: React vs Vue and React vs Svelte