A surface that slides up over the screen for a short, focused job — usually choosing one thing — and goes away when it is done.
Light
New rank
Choose a rankChoose a rank
White
One green stripe
Two green stripes
Green
Brown
Dark
New rank
Choose a rankChoose a rank
White
One green stripe
Two green stripes
Green
Brown
Source:
BottomSheet.tsx, exported from the barrel;ActionMenuandRankPickerSheetare built on it. The full picker spec: sheet-picker.
When to use it
- A picker for seven to twenty options, as rows; over twenty, with a pinned search.
- A short menu of actions behind a ⋯ — use
ActionMenu. - The native date and time pickers, which always sit in this sheet.
Not for two to six options (show chips), not for a form of its own (push a screen), and never two sheets stacked.
Anatomy
| Part | Token |
|---|---|
| Scrim | overlay.dim — the only thing separating the sheet from the screen; no shadow. The component passes the library a literal 0.35, which is the light value; dark's overlay.dim is 0.5 |
| Sheet | surface.card, radius.sheet 24 on the top corners only |
| Grabber | controlSize.grabberWidth 36 × controlSize.grabberHeight 4, border.control, radius.pill |
| Title | typography.title 22/700, left-aligned, 16 below the grabber and 16 above the first row |
| Rows | 64 floor, typography.rowTitle, hairline inset to the text, a check on the selected row |
| Height | sizes to its content by default; a sheet holding a scroll view passes snapPoints with enableDynamicSizing={false} |
States
- Open: slides up, the scrim fades in. Reduce-motion replaces the slide with a cross-fade.
- Selected row: a check on the right. A filled row inside a sheet reads as a hero.
- Closing: by pan-down, by tapping the scrim, or by choosing. Single-select closes on tap — the tap is the confirmation. All three leave the form underneath exactly as it was.
- Keyboard: the sheet rises with it when it holds a search field.
- Loading, error: the content's own states — skeleton rows, a bordered retry — inside the sheet.
Content
- The title is the question being answered: "Choose a rank". Left-aligned, never centred.
- No confirm button for a single choice. A multi-select sheet ends with one primary: "Done".
Accessibility
- Opening the sheet moves focus into it; closing returns focus to the trigger that opened it. A keyboard user who tabs into content behind the scrim is lost.
- The grabber is decoration. The scrim and a visible way to close are the accessible exits.
border.controlonsurface.cardis 3.53:1 in light, so the grabber is visible on the sheet.
Do and don't
White
One green stripe
Two green stripes
Green
Brown
Do
A check on the selected row.
White
One green stripe
Two green stripes
Green
Brown
Don’t
A filled row. Inside a sheet it reads as a hero, not a choice.
Code
import { useRef } from 'react';
import { BottomSheet, RankPicker, Text, type BottomSheetHandle } from '../../components/ui';
const sheet = useRef<BottomSheetHandle>(null);
<BottomSheet ref={sheet}>
<Text variant="title">Choose a rank</Text>
<RankPicker
ranks={ladder}
selectedSlug={rank?.slug}
onSelect={(next) => {
setRank(next);
sheet.current?.close();
}}
/>
</BottomSheet>