Bottom sheet

Free · MITReact Native · Expo

A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/bottom-sheet.json

Then run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/bottom-sheet.

  • Wrap the app in GestureHandlerRootView and ThemeProvider.

Usage

import { useState } from "react";
import { BottomSheet } from "@/components/goodui-native/bottom-sheet/bottom-sheet";
import { Button } from "@/components/goodui-native/button/button";

export function Filters() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onPress={() => setOpen(true)}>Show filters</Button>
      <BottomSheet
        open={open}
        onOpenChange={setOpen}
        title="Filters"
        description="Narrow 128 stays to what fits your trip"
        footer={<Button onPress={() => setOpen(false)}>Show 42 stays</Button>}
      >
        {/* rows */}
      </BottomSheet>
    </>
  );
}

When to use

Use it for
  • Secondary tasks on a phone: filters, details, share options, a short form.
  • Content that benefits from a peek height before expanding.
  • Lists inside a sheet: set scrollable and the sheet and list hand drags off to each other.
Reach for something else
  • A decision that must interrupt: use an alert dialog.
  • A full flow with several steps: push a screen instead.
  • A one-line confirmation of a background task: use toast.

Examples

A long list that hands off with the sheet

<BottomSheet open={open} onOpenChange={setOpen} title="Neighborhoods" detents={[0.45, 0.92]} scrollable>
  {places.map((p) => <Row key={p.id} {...p} />)}
</BottomSheet>

One fixed height

<BottomSheet open={open} onOpenChange={setOpen} title="Share trip" detents={[0.32]}>
  ...
</BottomSheet>

API reference

PropTypeDefault
open*
Whether the sheet is shown. Closing animates out before it unmounts.
boolean—
onOpenChange*
Called with false when the person drags it closed, taps the backdrop, presses Escape (web) or back.
(open: boolean) => void—
children*
Sheet content. Long content needs `scrollable`.
ReactNode—
title
Heading at the top of the sheet, also used in its accessibility label.
string—
description
Supporting line under the title.
string—
detents
Resting heights as fractions of the screen height. Default `[0.5, 0.92]`.
number[]—
initialDetent
Index into the sorted detents the sheet opens at.
number—
onDetentChange
Called when the sheet settles on a different detent.
(index: number) => void—
scrollable
Make the body a scroll view that hands off with the sheet: drags move the sheet until it is fully open, then scroll the content; pulling down at the top lowers it again.
boolean—
footer
Content pinned to the bottom of the screen at every detent, such as a primary button.
ReactNode—
dismissible
Allow closing by dragging down, tapping the backdrop, Escape on the web or the back button. Default `true`.
boolean—
labels
Overrides for the built-in screen reader strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<BottomSheetLabels>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • One shared value drives the position; drags follow the finger and releases project velocity to pick a detent or close.
  • Past the tallest detent the sheet rubber-bands with growing resistance.
  • The backdrop opacity is a function of position, so it darkens with the finger.
  • Reduced motion: detents snap with a short timing and the sheet fades in and out.
  • Haptic: select when a drag settles on a different detent
  • A closed sheet mounts nothing: no gestures, listeners or animated styles.
  • Gestures and position run on the UI thread; React re-renders only when open changes.

Accessibility

  • The grabber is an adjustable control: swipe up or down with a screen reader to change detents; double tap toggles between the smallest and tallest.
  • Detent changes are announced. The sheet surface is marked accessibilityViewIsModal.
  • The backdrop is a button labelled 'Close sheet' when the sheet is dismissible. Escape on the web and the Android back button close it first.
  • On the web the grabber is a slider with the detent as its value (arrow keys resize), and the scroll area is a focusable region named by the title.

Notes for AI

  • It renders into ThemeProvider's overlay layer, so ThemeProvider must wrap the app. It does not use a native Modal.
  • The app root must be wrapped in GestureHandlerRootView.
  • Control it with open and onOpenChange. Use footer for a primary action so it stays visible at every detent.
  • Set scrollable when content can exceed the sheet; do not nest your own ScrollView inside it.
  • Controlled inputs inside the sheet can keep their value in the screen: the overlay layer re-renders in the same commit as its owner, so fast typing never drops characters.