Skip to main content

Guide

Gorhom Bottom Sheet vs Expo Bottom Sheet vs RNSBS 2026

Gorhom Bottom Sheet v5 vs Expo Bottom Sheet vs React Native Simple Bottom Sheet compared. Snap points, dynamic content, haptics, and gestures in 2026.

·PkgPulse Team·
0

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 sizingenableDynamicSizing measures 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 Animated API

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

FeatureGorhom v5Expo Bottom SheetRNSBS
Animation engineReanimated 3 (UI thread)Reanimated 3RN 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 complexityMedium (GestureHandler)LowLow
Bundle sizeLargeMediumSmall
TypeScript✅ Excellent
GitHub stars7.5kN/A (new)0.5k
npm weekly700kGrowing20k

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
  • enableDynamicSizing to fit variable-height content
  • BottomSheetModal for 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-ui components
  • 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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.