BetaOverlays

BottomSheet

A surface that comes up from the bottom edge and can be thrown back down.

Overview

A surface that comes up from the bottom edge and can be thrown back down.

First example

<BottomSheet>
<BottomSheet.Trigger asChild>
<Button>Partager</Button>
</BottomSheet.Trigger>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Handle />
<BottomSheet.Title>Partager ce document</BottomSheet.Title>
</BottomSheet.Content>
</BottomSheet>

Anatomy

<BottomSheet>
  <BottomSheet.Trigger>…</BottomSheet.Trigger>
  <BottomSheet.Overlay />
  <BottomSheet.Content>
    <BottomSheet.Handle />
    <BottomSheet.Title>…</BottomSheet.Title>
    <BottomSheet.Description>…</BottomSheet.Description>
    <BottomSheet.Close>…</BottomSheet.Close>
  </BottomSheet.Content>
</BottomSheet>
  • BottomSheet — state and resolved style. It renders no node.
  • BottomSheet.Trigger — what brings it up.
  • BottomSheet.Overlay — the backdrop. It dims, and closes on a press.
  • BottomSheet.Content — the sheet, and the gesture.
  • BottomSheet.Handle — the grab bar.
  • BottomSheet.Title / BottomSheet.Description — its two texts.
  • BottomSheet.Close — anything that sends it back down.

Usage

Basic

<BottomSheet>
  <BottomSheet.Trigger asChild>
    <Button>Partager</Button>
  </BottomSheet.Trigger>
  <BottomSheet.Overlay />
  <BottomSheet.Content>
    <BottomSheet.Handle />
    <BottomSheet.Title>Partager ce document</BottomSheet.Title>
  </BottomSheet.Content>
</BottomSheet>

One that cannot be escaped

<BottomSheet.Overlay isDismissable={false} />
<BottomSheet.Content isSwipeable={false}>…</BottomSheet.Content>

Two separate refusals, because a sheet that can be tapped away but not dragged is a real design, and so is the reverse.

Style as props

<BottomSheet.Content padding={24} />
<BottomSheet.Handle width={48} />

Full RN names, full RN values (R14). Every node takes them.

Root props

BottomSheetProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
radiusRadiusKey | undefinedNo
isOpenboolean | undefinedNo
defaultOpenboolean | undefinedNo
onOpenChange((isOpen: boolean) => void) | undefinedNo
isDisabledboolean | undefinedNo
dismissThresholdnumber | undefinedNoHow far down the sheet has to be dragged before letting go closes it, as a fraction of its own height.
collapsedHeightnumber | undefinedNoHow much of a long sheet shows when it is reduced, in points. Setting it gives the sheet a second state between up and gone: the tail below this height slides off the bottom of the screen and comes back when it expands. The sheet is **not re-laid out** — it is the same box, moved — so what is cut is cut wherever the line happens to fall. Left unset the sheet has no reduced state and behaves as it always has.
isExpandedboolean | undefinedNoWhether the sheet is at its full height. Only means anything alongside `collapsedHeight` — without one there is nothing to be reduced to.
defaultExpandedboolean | undefinedNo
onExpandedChange((isExpanded: boolean) => void) | undefinedNo

Slots

BottomSheetCloseProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

BottomSheetContentProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
isSwipeableboolean | undefinedNoWhether dragging the sheet down closes it.

BottomSheetDescriptionProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

BottomSheetHandleProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

BottomSheetOverlayProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
isDismissableboolean | undefinedNo

BottomSheetSummaryProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

BottomSheetTitleProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

BottomSheetTriggerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

The sheet is accessibilityViewIsModal, so a screen reader stops at it rather than reading the page behind — the spoken half of what the backdrop says visually. The title is a header. The handle and the overlay announce nothing: a pill carries no information, and the drag it stands for is not a gesture a screen reader performs.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

How the gesture works

It measures its own height, then slides that far. A sheet is as tall as what is in it, and nothing else on the screen knows that number — the first layout is what tells the animation how far "down" is. Until it has one the sheet waits off-screen at a pessimistic distance rather than flashing at its resting place for a frame.

Downward only. A sheet dragged upward has nowhere to go — it is already against the top of its own content — and letting it stretch there is a rubber-band nobody asked for.

Far enough or fast enough. Past dismissThreshold of its height it closes; so does a flick over 900 points a second, whatever the distance. Without the second, a quick flick from the top of a tall sheet is refused however clearly it meant to throw the thing away. Anything short of either springs back.

A reduced state

<BottomSheet collapsedHeight={200} defaultExpanded={false}>
  <BottomSheet.Content>
    <BottomSheet.Handle accessibilityLabel="Réduire ou déplier la fiche" />…
  </BottomSheet.Content>
</BottomSheet>

collapsedHeight gives the sheet a second disclosure inside the first: it is either up or gone, and while it is up it is either full or reduced. isExpanded, defaultExpanded and onExpandedChange control it the way isOpen controls the other one.

These are not snap points. There are two heights, not an array of them, and the reduced one is a number you give rather than a fraction of the screen the sheet works out.

The sheet is not re-laid out. It is the same box at its full height, moved further down, so the tail below the reduced height slides off the bottom of the screen and comes back untouched. Nothing re-measures — which is why a bare collapsedHeight cuts wherever the number happens to land, and why BottomSheet.Summary below exists.

BottomSheet.Summary — cut at a seam you chose

<BottomSheet defaultExpanded={false}>
  <BottomSheet.Content>
    <BottomSheet.Handle accessibilityLabel="Réduire ou déplier la fiche" />
    <BottomSheet.Summary>
      <BottomSheet.Title>Café des Arts</BottomSheet.Title>
      <BottomSheet.Description>★★★★☆ · Ouvert jusqu'à 22 h</BottomSheet.Description>
    </BottomSheet.Summary>
    <Hours />
    <Reviews />
  </BottomSheet.Content>
</BottomSheet>

It is <summary> to the sheet's <details>, and the same thing an Accordion.Trigger is: the part that survives, not a second view for the reduced state. It renders in both — what changes is whether everything under it does — so it costs no extra layout.

It reports where its bottom edge falls, not how tall it is, so whatever sits above it is counted too: a handle above a summary stays visible when the sheet reduces. That is why it must be a direct child of Contenty is relative to the immediate parent, and a summary wrapped in a View would report the wrapper's coordinates.

The sheet adds its own bottom padding back onto that edge. Cutting on the summary's last pixel would leave the reduced sheet with air above the handle and none at all under the last line — the text against the screen edge, and under the gesture bar on a phone that has one. The reduced sheet ends with the padding its expanded self ends with, and overriding the sheet's padding moves the seam with it.

collapsedHeight is not extended that way. It is a number you wrote against a sheet you were looking at, and two hundred points showing has to mean two hundred.

Give both and the summary wins, with a warning in development: a measurement of the content is truer than a number that has to be kept in step with it.

Where a drag goes

A drag that was not decisive puts the sheet back, whatever distance it covered. Decisive is past dismissThreshold of the sheet's height or faster than 900 points a second, either alone being enough.

fromdecisive downdecisive up
expandedcollapsed — or closed, if the throw was aimed past the notch
collapsedclosedexpanded

The exception in the first row is the one thing a strict one-state-per-drag rule gets wrong: dragging a sheet the whole way to the bottom and having it stop half open reads as a refusal. Where the throw was aimed is the release point plus 0.15 s of its velocity.

Without a collapsedHeight there is no middle row and no exception — the sheet behaves exactly as it always has.

The handle becomes a control

On a collapsible sheet BottomSheet.Handle is pressable, the way an Accordion.Trigger is, and a press toggles the two heights. That is not decoration acquiring a behaviour by accident: a drag would otherwise be the only way in and out of the reduced state, and a drag is a gesture some people cannot perform. It carries accessibilityRole="button" and accessibilityState={{ expanded }}, and warns in development without an accessibilityLabel — a pill says nothing to someone who cannot see it.

Without a collapsedHeight the handle stays what it was: a pill, hidden from screen readers, taking no touches.

The backdrop does not know about any of this. A reduced sheet is often a persistent panel rather than a modal, and a dimmed page behind one reads oddly — leave BottomSheet.Overlay out, or drive its own props, if that is the sheet you are building.

Not `@gorhom/bottom-sheet`

The reference implementation wraps it. A sheet that slides, springs and dismisses is a pan gesture and a shared value; taking a dependency for that would put a second animation library in every app that installs one component. What we lose is their scroll integration, which is worth having and worth its own change rather than a dependency.

Their snap points we do not have and are not planning: collapsedHeight covers the case they are almost always used for, with two named states instead of an array of positions.