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.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| radius | RadiusKey | undefined | No | — |
| isOpen | boolean | undefined | No | — |
| defaultOpen | boolean | undefined | No | — |
| onOpenChange | ((isOpen: boolean) => void) | undefined | No | — |
| isDisabled | boolean | undefined | No | — |
| dismissThreshold | number | undefined | No | How far down the sheet has to be dragged before letting go closes it, as a fraction of its own height. |
| collapsedHeight | number | undefined | No | How 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. |
| isExpanded | boolean | undefined | No | Whether the sheet is at its full height. Only means anything alongside `collapsedHeight` — without one there is nothing to be reduced to. |
| defaultExpanded | boolean | undefined | No | — |
| onExpandedChange | ((isExpanded: boolean) => void) | undefined | No | — |
Slots
BottomSheetCloseProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
BottomSheetContentProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| isSwipeable | boolean | undefined | No | Whether 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.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
BottomSheetHandleProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
BottomSheetOverlayProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| isDismissable | boolean | undefined | No | — |
BottomSheetSummaryProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
BottomSheetTitleProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
BottomSheetTriggerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
Variants, sizes and colour
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
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 Content — y 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.
| from | decisive down | decisive up |
|---|---|---|
| expanded | collapsed — or closed, if the throw was aimed past the notch | — |
| collapsed | closed | expanded |
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.