Shipnative
ShipnativeShipnative
Sign in
GuideAugust 2026 · 8 min read

React Native Bottom Sheet: The Working Setup (2026)

The bottom sheet became the default mobile pattern for a reason — it shows detail without leaving the screen, and it’s reachable with a thumb. It’s also one of the fiddliest components to get right: a drag that has to track your finger perfectly, a scroll view that must hand control back to the drag at the top, a keyboard that wants to move the same view, and three separate setup steps that each fail silently. Here’s the version that works, plus the specific reasons your sheet is invisible.

First: do you need the library?

OptionSnap pointsScrollable contentNative sheetUse it for
@gorhom/bottom-sheetAny number of snap pointsBuilt-in scroll viewsNo — drawn in JS/ReanimatedMaps, filters, players, anything with a half-open state
Expo Router modal presentationiOS detents onlyNormal ScrollViewYes — real system sheetA screen that just needs to slide up
React Native ModalNoneNormal ScrollViewPartlyConfirm dialogs, not sheets

If the sheet is really just a screen that slides up and gets dismissed, an Expo Router modal route is less code and gives you a genuine platform sheet. The moment you need a half-open state — a map with a peeking result list, a player with a mini bar — you need real snap points, and that means @gorhom/bottom-sheet.

Step 1 — Install all three packages

npx expo install @gorhom/bottom-sheet \
  react-native-reanimated react-native-gesture-handler

The sheet is not a standalone component — it’s a Reanimated animation driven by a Gesture Handler pan. Installing only the sheet package produces a module-not-found error at best and a component that renders but won’t drag at worst. Use npx expo install rather than npm install so the native versions match your SDK.

Step 2 — Two providers at the root

This is the step people skip, and it’s the reason for most “the sheet does nothing” bug reports. In an Expo Router app, put both in the root layout:

// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <BottomSheetModalProvider>
        <Stack />
      </BottomSheetModalProvider>
    </GestureHandlerRootView>
  );
}

style={{ flex: 1 }} on the root view is not optional. Without it the view collapses to zero height and everything inside — including your entire app — disappears, which is a memorable five minutes of debugging.

Step 3 — A complete sheet

import { useCallback, useMemo, useRef } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import BottomSheet, {
  BottomSheetModal, BottomSheetBackdrop, BottomSheetScrollView,
} from '@gorhom/bottom-sheet';

export default function PlaceScreen({ place }) {
  const sheetRef = useRef<BottomSheetModal>(null);

  // memoize: a new array each render re-computes the snap layout
  const snapPoints = useMemo(() => ['25%', '60%', '90%'], []);

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

  return (
    <View style={{ flex: 1 }}>
      <Button title="Show details" onPress={() => sheetRef.current?.present()} />

      <BottomSheetModal
        ref={sheetRef}
        index={1}                       // open at 60%
        snapPoints={snapPoints}
        enablePanDownToClose
        backdropComponent={renderBackdrop}
        backgroundStyle={styles.sheetBackground}
        handleIndicatorStyle={styles.handle}
        keyboardBehavior="interactive"
        keyboardBlurBehavior="restore"
        onChange={(i) => { if (i === -1) console.log('closed'); }}
      >
        <BottomSheetScrollView contentContainerStyle={styles.content}>
          <Text style={styles.title}>{place.name}</Text>
          <Text style={styles.body}>{place.description}</Text>
        </BottomSheetScrollView>
      </BottomSheetModal>
    </View>
  );
}

const styles = StyleSheet.create({
  sheetBackground: { backgroundColor: '#16161a' },
  handle: { backgroundColor: '#666', width: 44 },
  content: { padding: 20, paddingBottom: 48, gap: 12 },
  title: { color: '#fff', fontSize: 22, fontWeight: '700' },
  body: { color: 'rgba(255,255,255,0.7)', fontSize: 15, lineHeight: 22 },
});

Note BottomSheetScrollView, not the normal one. The sheet needs to know your scroll position so it can decide whether a downward drag scrolls the content or closes the sheet. A plain ScrollView swallows the gesture and the sheet stops responding to drags entirely — the second-most-reported problem after the missing root view.

The traps, in the order you’ll hit them

  1. Nothing drags. Missing GestureHandlerRootView, or it’s not at the true root.
  2. “present()is not a function”. You’re using BottomSheetModal without BottomSheetModalProvider, or you called present() on a plain BottomSheet, which uses snapToIndex() instead.
  3. The sheet is there but invisible. No backgroundStyle, so it’s the default white on a white screen — or in dark mode, dark on dark. Always set it explicitly.
  4. Content is cut off at the bottom.The sheet doesn’t apply the home-indicator inset for you. Add bottom padding from useSafeAreaInsets(), or a fixed paddingBottom as above.
  5. The keyboard covers the input. Use BottomSheetTextInput and the two keyboard* props. See the form validation guide for the rest of the keyboard story.
  6. The sheet re-snaps oddly on re-render. An inline snapPoints={['25%', '60%']} array is a new array every render. Memoize it.

If your content height varies, skip percentages entirely: pass enableDynamicSizingand let the sheet measure the content. It saves you hard-coding a snap point that’s wrong on a small screen.

The shortcut: generate the wiring, keep the taste

Everything above is setup — two providers, three packages, six props with specific names. None of it is a product decision. The product decisions are where the snap points sit and what’s visible at each one, and those you can only judge with a thumb on a real screen.

Describe the interaction in ShipNative — “a map of nearby cafés where tapping a pin opens a draggable detail sheet with photos and hours” — and it wires the providers, the sheet, the backdrop and the scroll view, then runs it on your phone so you can drag it. Adjust by prompting; export the full Expo project when you’re happy.

Build it free

Describe your app in one sentence and have it running on your phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

What is the best bottom sheet library for React Native?

@gorhom/bottom-sheet is the standard. It is built on Reanimated and Gesture Handler, so the drag runs on the UI thread and tracks your finger exactly, and it handles snap points, backdrops, keyboard avoidance, and scrollable content. The main alternatives are the platform-native sheets you get from Expo Router modal presentation, which are simpler but far less configurable.

Why is my bottom sheet invisible or not draggable?

Three causes, in order of likelihood: the app is not wrapped in GestureHandlerRootView, so no gestures reach the sheet; the sheet has no explicit background style and is rendering white-on-white; or you are using BottomSheetModal without a BottomSheetModalProvider above it. Check those three before debugging anything else.

Do I need a bottom sheet library at all?

Not always. If you only need a simple modal that slides up and dismisses, Expo Router supports native modal presentation and iOS gives you a real system sheet with detents. Reach for @gorhom/bottom-sheet when you need multiple snap points, a persistent partially-open state, or content that scrolls inside the sheet.

How do I keep a bottom sheet above the keyboard?

Pass keyboardBehavior="interactive" and keyboardBlurBehavior="restore" to the sheet, and use the BottomSheetTextInput component instead of a plain TextInput. Using a plain TextInput inside a sheet is the usual reason the keyboard covers the field it is supposed to reveal.

Can an AI app builder add a bottom sheet for me?

Yes. Describe the interaction — "tapping a place on the map opens a draggable detail sheet" — and ShipNative wires the provider, the gesture root, the snap points, and the backdrop, then previews it on your phone. Sheets are worth judging on hardware, since the drag feel is most of the design.

→

React Native Animations with Reanimated

The shared values and worklets a bottom sheet is built on.

Read guide →
→

Add Maps to a React Native App

Map plus detail sheet is the single most common pairing.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.