React Native Bottom Sheet Guide: @gorhom/bottom-sheet v5 and the Native Alternatives (2026)

A practical React Native bottom sheet guide: @gorhom/bottom-sheet v5 snap points, modals, scrolling and keyboard, plus Expo UI, formSheet and True Sheet.

React Native Bottom Sheet Guide: @gorhom/bottom-sheet v5 and the Native Alternatives (2026)

Every app eventually gets a bottom sheet. Sometimes it’s a filter panel, sometimes a share menu or a map card that peeks up from the bottom, and once in a while it’s a whole checkout flow that product insists should “slide up”. React Native has no bottom sheet in core, so the choice falls to you, and in 2026 the honest answer is no longer “just install gorhom”. This guide covers React Native bottom sheet implementations with @gorhom/bottom-sheet v5, which most apps still use, and then the native alternatives that have caught up this year.

Versions were checked on npm on October 12, 2026: @gorhom/bottom-sheet 5.2.14, react-native-reanimated 4.7.1, react-native-gesture-handler 3.3.0 (2.33.0 on the legacy tag), @expo/ui 57.0.22 on SDK 57, @lodev09/react-native-true-sheet 3.11.18.

Pick the kind of sheet before you pick the library

Two things that look the same on a design mock behave very differently in code.

  • An inline sheet lives inside a screen and is always there, collapsed or expanded. The map app pattern: a card at 15% that you drag up to 50% and 90% while the map stays interactive above it.
  • A modal sheet appears over everything, dims the content behind it and goes away. Share menus, filters, confirm dialogs, a short form.

Native platforms are very good at the second kind and do not offer the first. That single fact explains most of the library landscape: @gorhom/bottom-sheet draws everything in JavaScript and Reanimated, so it can do both, while the native options do modal sheets only and do them better.

Option 1: @gorhom/bottom-sheet v5

Gorhom’s library is the default for a reason. It sits at about 3 million downloads a week, it is MIT licensed, and the API covers almost every sheet a designer has drawn. Version 5.2.14 was published on May 9, 2026. More on what five quiet months mean further down.

Install

npm install @gorhom/bottom-sheet@^5
# peers, if you do not have them already
npx expo install react-native-reanimated react-native-gesture-handler

The peer ranges are react-native-reanimated >=3.16.0 || >=4.0.0- and react-native-gesture-handler >=2.16.1. On Reanimated 4 you also need react-native-worklets, which Expo installs for you. Then wrap the root of your app once:

// App.tsx or app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function Root() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* navigation, providers, screens */}
    </GestureHandlerRootView>
  );
}

An inline sheet with snap points

import { useCallback, useMemo, useRef } from 'react';
import { Text, View } from 'react-native';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';

export function MapScreen() {
  const sheetRef = useRef<BottomSheet>(null);
  const snapPoints = useMemo(() => ['15%', '50%', '90%'], []);

  const handleChange = useCallback((index: number) => {
    console.log('sheet index', index);
  }, []);

  return (
    <View style={{ flex: 1 }}>
      {/* your map here */}
      <BottomSheet
        ref={sheetRef}
        index={0}
        snapPoints={snapPoints}
        enableDynamicSizing={false}
        onChange={handleChange}
      >
        <BottomSheetView style={{ padding: 16 }}>
          <Text>3 coffee shops nearby</Text>
        </BottomSheetView>
      </BottomSheet>
    </View>
  );
}

Three details matter here. index is the snap point the sheet starts on, and -1 means closed. snapPoints lives in useMemo, because a new array on every render makes the sheet recalculate its positions. And enableDynamicSizing={false} is there because in v5 dynamic sizing is on by default, which means the sheet measures its content and adds that height as an extra snap point. With fixed snap points you almost never want that, and it explains a large share of the “my sheet stops at a weird height” issues people open.

To move the sheet from code, use the ref: snapToIndex(n), expand(), collapse(), close().

A modal sheet

Modals need a provider, which should sit inside GestureHandlerRootView and outside your navigator, so that a sheet opened from any screen renders above all of them:

import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';

<GestureHandlerRootView style={{ flex: 1 }}>
  <BottomSheetModalProvider>
    <RootNavigator />
  </BottomSheetModalProvider>
</GestureHandlerRootView>

Then, anywhere below it:

import { useCallback, useRef } from 'react';
import { Button, Text } from 'react-native';
import {
  BottomSheetBackdrop,
  BottomSheetBackdropProps,
  BottomSheetModal,
  BottomSheetView,
} from '@gorhom/bottom-sheet';

export function ShareButton() {
  const modalRef = useRef<BottomSheetModal>(null);

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        appearsOnIndex={0}
        disappearsOnIndex={-1}
        pressBehavior="close"
      />
    ),
    []
  );

  return (
    <>
      <Button title="Share" onPress={() => modalRef.current?.present()} />
      <BottomSheetModal
        ref={modalRef}
        backdropComponent={renderBackdrop}
        enablePanDownToClose
      >
        <BottomSheetView style={{ padding: 24 }}>
          <Text>Share this post</Text>
        </BottomSheetView>
      </BottomSheetModal>
    </>
  );
}

No snap points here, so dynamic sizing does its job and the sheet fits the content. That is what you want for menus and short forms. The backdrop is off by default, so you have to pass one if you want the dimmed background. present() and dismiss() replace the index juggling, and useBottomSheetModal() gives you dismissAll() for the “close everything” case after a successful action.

Scrolling content

A regular ScrollView or FlatList inside a sheet will fight the pan gesture: you drag down to scroll up and the sheet closes instead. Use the library’s scrollables, which coordinate with the sheet’s gesture:

import { BottomSheetFlatList } from '@gorhom/bottom-sheet';

<BottomSheet index={1} snapPoints={snapPoints} enableDynamicSizing={false}>
  <BottomSheetFlatList
    data={shops}
    keyExtractor={(item) => item.id}
    renderItem={({ item }) => <ShopRow shop={item} />}
  />
</BottomSheet>

There are BottomSheetScrollView, BottomSheetFlatList, BottomSheetSectionList, BottomSheetVirtualizedList and BottomSheetFlashList. Long scrollable content and dynamic sizing do not mix well, so with a long list, set fixed snap points and turn dynamic sizing off. If you want dynamic sizing with a ceiling, use maxDynamicContentSize instead.

Text inputs and the keyboard

This is the part that eats an afternoon. Use BottomSheetTextInput instead of TextInput so the sheet knows about the keyboard, then choose what should happen when it opens:

import { BottomSheetTextInput } from '@gorhom/bottom-sheet';

<BottomSheetModal
  ref={modalRef}
  keyboardBehavior="interactive"
  keyboardBlurBehavior="restore"
  android_keyboardInputMode="adjustResize"
>
  <BottomSheetView style={{ padding: 16 }}>
    <BottomSheetTextInput placeholder="Add a comment" style={styles.input} />
  </BottomSheetView>
</BottomSheetModal>

keyboardBehavior is 'interactive' (the sheet moves up by the keyboard height), 'extend' (it goes to its highest snap point) or 'fillParent' (it takes the whole screen). keyboardBlurBehavior="restore" puts the sheet back where it was when the keyboard closes. On Android, android_keyboardInputMode="adjustResize" is the setting that usually makes the input stop hiding behind the keyboard. Test this one on a real Android device with gesture navigation, not only the emulator.

The maintenance question

The last release, 5.2.14, came out in May 2026, after a burst of patches in April. Five months without a publish is not abandonment for a library this mature, and the peer range already covers Reanimated 4. Two things deserve watching, though. First, Gesture Handler 3.x is now the latest tag, and 2.33.0 has moved to legacy, with 2.x support ending at React Native 0.87. The sheet’s peer range of >=2.16.1 technically accepts 3.x, but a peer range is not a test suite, so check the issue tracker before you upgrade both at once. Second, the most credible replacement now comes from Expo, which shipped a drop-in with the same API (option 2 below). I would still pick gorhom for new inline sheets today. I would also keep every gorhom import in one wrapper file so a swap later is a ten-minute job.

Option 2: @expo/ui’s gorhom-compatible BottomSheet

On Expo SDK 57, @expo/ui ships a BottomSheet with an API compatible with @gorhom/bottom-sheet. It renders a Jetpack Compose ModalBottomSheet on Android, a SwiftUI sheet on iOS and a vaul drawer on web. The migration in the Expo docs is an import change:

// before
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
// after
import BottomSheet, { BottomSheetView } from '@expo/ui/community/bottom-sheet';

It needs no GestureHandlerRootView, it exports BottomSheetModal with present() and dismiss(), and without snapPoints it fits its content. The fine print matters more than the headline, though:

  • It is always a modal presentation on native. If your sheet is the inline map-card kind, this is not a replacement.
  • On Android, snap points collapse to two states, partial and expanded, because that is all Compose’s modal sheet supports.
  • BottomSheetBackdrop, BottomSheetHandle, BottomSheetFooter, BottomSheetFlashList, BottomSheetVirtualizedList, useBottomSheetModal and the spring and timing config hooks are not supported.

For simple modal sheets on Expo it is the better result: real platform sheets, platform gestures, no JavaScript animation on the UI thread. If you are starting fresh and don’t need gorhom compatibility, Expo UI also has a universal BottomSheet from @expo/ui with a controlled isPresented and onDismiss API, plus 'half' and 'full' snap points. Our Expo explainer covers how @expo/ui fits into the SDK.

Option 3: a native sheet as a route

If the sheet is really a screen (it has its own URL, a back gesture, maybe a deep link), let the navigator present it. With the native stack from React Navigation, which sits on react-native-screens:

<Stack.Screen
  name="Filters"
  component={FiltersScreen}
  options={{
    presentation: 'formSheet',
    sheetAllowedDetents: [0.5, 1],
    sheetGrabberVisible: true,
    sheetCornerRadius: 24,
  }}
/>

Expo Router takes the same options on Stack.Screen. You get the real iOS sheet, with its exact physics, and navigation state handles open and closed for you, so there is no ref to keep in sync. The cost is that the sheet is a route: passing data in and out goes through params or shared state, not props. For filter panels and detail sheets that is a fair trade. Our modals guide compares this with the core Modal and other navigation-level modals.

Option 4: react-native-true-sheet

@lodev09/react-native-true-sheet (3.11.18, October 2, about 180K downloads a week) wraps the platform sheet controllers and keeps a component API:

import { useRef } from 'react';
import { Button } from 'react-native';
import { TrueSheet } from '@lodev09/react-native-true-sheet';

export function Example() {
  const sheet = useRef<TrueSheet>(null);

  return (
    <>
      <Button title="Open" onPress={() => sheet.current?.present()} />
      <TrueSheet ref={sheet} detents={['auto', 1]}>
        <Button title="Close" onPress={() => sheet.current?.dismiss()} />
      </TrueSheet>
    </>
  );
}

present() and dismiss() return promises, which is handy for “close the sheet, then navigate” flows. 'auto' sizes to the content. It works with bare React Native and with Expo dev builds. A v4 beta (4.0.0-beta.24) is in progress: it replaces the boolean scrollable prop with a scrollableRef, so pin to ^3 unless you want to follow the beta.

Five bugs you will probably hit

  1. The sheet renders but ignores gestures. GestureHandlerRootView is missing, or it does not wrap the tree the sheet is in. With modals, the provider must sit inside it.
  2. The sheet stops at a height you never asked for. v5 dynamic sizing is adding a snap point from your content height. Set enableDynamicSizing={false} when you pass snapPoints.
  3. Scrolling closes the sheet. You used a plain FlatList. Swap it for BottomSheetFlatList.
  4. The modal shows up behind a screen. The BottomSheetModalProvider is inside one screen instead of around the navigator, or you opened it from a native modal screen, which sits in its own window on iOS. Move the provider up first. Opening a sheet from inside a native modal usually means that screen should present its own sheet.
  5. Everything is slow. Snap points or callbacks are recreated on every render, or the sheet mounts and unmounts with a conditional instead of staying mounted at index={-1}. Memoize, keep it mounted, and avoid heavy children in a sheet that is closed most of the time.

Which one should you use?

You need Use
Inline, always-visible sheet over a map or feed @gorhom/bottom-sheet BottomSheet
Custom-designed modal sheet with footer, custom handle, animated backdrop @gorhom/bottom-sheet BottomSheetModal
Simple modal sheet on Expo, platform look @expo/ui BottomSheet
Migrating gorhom modals to native on Expo @expo/ui/community/bottom-sheet
A sheet that is really a screen Native stack presentation: 'formSheet'
Native sheet with an imperative API, bare or Expo react-native-true-sheet

My default for a new app in late 2026: gorhom for the one or two inline sheets the design really needs, and native sheets (form sheet routes, or Expo UI on Expo) for everything modal. Native sheets get platform behaviour like iOS 26 Liquid Glass and Android predictive back for free, and that list will keep growing. If your sheet holds a selector, our dropdown and picker comparison covers what to put inside it.

FAQ

Does React Native have a built-in bottom sheet?

No. Core has Modal with animationType="slide", which looks like a sheet but has no snap points or drag-to-dismiss. For a real bottom sheet, use a library or a native stack screen with presentation: 'formSheet'.

What is the best bottom sheet library for React Native?

@gorhom/bottom-sheet is the most flexible and the most used, and it is the only common choice for inline sheets. For modal sheets with native behaviour, use Expo UI’s BottomSheet on Expo or react-native-true-sheet in bare projects.

Does @gorhom/bottom-sheet work with Reanimated 4?

Yes. Version 5’s peer range includes Reanimated 4, which also needs react-native-worklets installed.

Why does my bottom sheet open at the wrong height?

In v5 dynamic sizing is on by default and adds your content height as a snap point. Pass enableDynamicSizing={false} when you set fixed snapPoints, and wrap static content in BottomSheetView when you do want it sized to fit.

How do I stop the keyboard covering an input in a bottom sheet?

Use BottomSheetTextInput, set keyboardBehavior to 'interactive' or 'extend', and on Android set android_keyboardInputMode="adjustResize".

Leave a Reply 0

Your email address will not be published. Required fields are marked *