Composition

The anatomy of a sheet, what each part does, and every prop each one takes.

A sheet is a root and the parts placed inside it. The root does all of the arithmetic — the snap points, the keyboard lift, where the footer sits — and publishes it through three contexts. A part reads those and renders one thing, and every part accepts style and ref.

import { BottomSheet } from "@delacour/react-native-bottom-sheet";

<BottomSheet>
  <BottomSheet.Trigger />
  <BottomSheet.Portal>
    <BottomSheet.Overlay />
    <BottomSheet.Container>
      <BottomSheet.Background />
      <BottomSheet.Handle />
      <BottomSheet.Content>
        <BottomSheet.Title />
        <BottomSheet.Description />
        <BottomSheet.TextInput />
        <BottomSheet.Close />
      </BottomSheet.Content>
      <BottomSheet.Footer />
    </BottomSheet.Container>
  </BottomSheet.Portal>
</BottomSheet>;

The render tree

BottomSheet              the root — state, geometry, animation, the pans, the ref; renders nothing
  Trigger                anywhere in the app's tree; opens the sheet
  Portal                 the frame — teleported to the nearest host, or in place with `inline`
    Overlay              the scrim, written BEFORE the panel so hit-testing gives the panel its taps
    Container            the panel — the one view that moves
      Background         an absolute fill under everything; where the surface colour and radius go
      Handle             the grabber row; a pan detector and the sheet's one adjustable element
      Content            the static body; a pan detector; clamped to the area above the footer
        Title            labels the panel for assistive technology
        Description      supporting copy
        TextInput        a field the sheet owns the keyboard for
        Close            dismisses
      Footer             sticky over the body, riding the keyboard; or a plain view that scrolls

The same parts, as they are laid out on the screen:

  +-- Portal: the frame ---------------------+
  |                                          |
  |   Overlay: fills the frame, behind       |
  |                                          |
  | +-- Container: the panel, the one -----+ |
  | |   view that moves                    | |
  | | +----------------------------------+ | |
  | | | Handle          ====             | | |
  | | +----------------------------------+ | |
  | | | Content                          | | |   Background fills the panel
  | | |   Title                          | | |   under all three
  | | |   Description                    | | |
  | | +----------------------------------+ | |
  | | | Footer: absolute, over the body  | | |
  | | +----------------------------------+ | |
  +-+--------------------------------------+-+

Exactly one body: Content, or one of the scrollables, or Steps. Footer is written beside the body, not inside it, so it can stay put while the body scrolls under it.

Container is the moving panel and Content is the static body. That is the naming the skinned library uses too, so a sheet written against one reads against the other.

The overlay is written first

Native hit-testing walks siblings back to front, so a touch on the panel never reaches the overlay and a touch beside it does. The overlay is a React Native Pressable, never a Gesture Handler tap: a full-screen tap gesture behind the sheet wins a touch on a TextInput, which has no gesture of its own, and the field never focuses. Omit Overlay and the sheet has no scrim, and the app behind it stays interactive.

Context reaches every part

The three contexts are provided once, by the root. A teleported portal moves the native view and leaves the React tree where it was, so they reach every part without being re-provided — and so does every context your app provides around the trigger: a theme, a form, a query client.

import { useBottomSheet, useBottomSheetAnimated } from "@delacour/react-native-bottom-sheet";
import Animated, { useAnimatedStyle } from "react-native-reanimated";

function Chevron() {
  const { index, snapToIndex } = useBottomSheet();
  const { animatedIndex } = useBottomSheetAnimated();
  const style = useAnimatedStyle(() => ({ transform: [{ rotate: `${animatedIndex.value * 180}deg` }] }));
  return <Animated.View onTouchEnd={() => snapToIndex(index === 0 ? 1 : 0)} style={style} />;
}
HookReturnsThread
useBottomSheet()isOpen, setOpen, index, snapPointCount, and every ref methodJS — React state, updated on settle
useBottomSheetAnimated()animatedIndex, animatedPosition, animatedHeight, containerHeight, handleHeight, contentHeight, footerHeight, keyboardLift, snapPoints, closedHeightUI — shared values, for useAnimatedStyle
useBottomSheetInternal()what the parts share, for a part written outside the package

Each throws outside a <BottomSheet>; the useOptional… form returns null instead.

The ref

const sheet = useRef<BottomSheetRef>(null);

sheet.current?.snapToIndex(1);
sheet.current?.snapToPosition("50%");
sheet.current?.expand();
sheet.current?.collapse();
sheet.current?.open();
sheet.current?.close();
sheet.current?.dismiss(); // close, under the name the library this replaces used
sheet.current?.forceClose(); // no animation

Every method is an intent: none animates from the JS thread, and each is a no-op when it makes no sense — close on a closed sheet, snapToIndex(4) with two snap points. snapToPosition clamps to the available height and never adds a snap point. open and close are the same state a controlled isOpen drives.

API Reference

BottomSheet

Prop

Type

Trigger

Opens the sheet. A React Native Pressable around its children, or with asChild the child itself with onPress donated to it.

Prop

Type

Portal

The frame the sheet measures itself against. Teleported to the nearest host under a provider; in place with inline or without one. See Portal and host.

Prop

Type

Overlay

The scrim. Its opacity follows the keyboard-free index on the UI thread, so a keyboard lift never dims the app and a higher snap point is no darker.

Prop

Type

Container

The panel — the one view that moves. accessibilityViewIsModal while open under an overlay, its descendants hidden from assistive technology while closed, labelled by the Title.

Prop

Type

Background

An absolute fill under everything in the panel, pointerEvents: none. Where the surface colour and the corner radii go. Exactly the sheet's height tall when detached, so a floating card's bottom corners are real.

Prop

Type

Handle

The grabber's row: a pan detector, and the sheet's one adjustable accessibility element — increment and decrement move a snap point, escape closes, and its value reads Snap point i of n. Put the pill inside it.

Prop

Type

Content

The static body: a pan detector, clamped to the area above the footer and the keyboard, with an inner box that measures the dynamic snap point.

Prop

Type

Sticky over the body by default, riding the keyboard by transform. See Footer.

Prop

Type

TextInput

React Native's TextInput with useBottomSheetTextInput() already spread on it. See Keyboard.

Prop

Type

Close

Dismisses. A Pressable, or with asChild the child with onPress donated.

Prop

Type

Title and Description

A Text each. Title publishes the nativeID the panel is labelled by; Description publishes one of its own. Both take asChild to hand those to a text of yours.

Prop

Type

Host and Provider

BottomSheet.Host and BottomSheet.Provider are named on the compound from src/portal; they are documented on Portal and host. ScrollView, FlatList and SectionList are on Scrollables; Steps and Step on Steps.

On this page