<!-- https://getkonjo.com/design/components/bottom-sheet · source: docs/design/components/bottom-sheet.md -->

# Bottom sheet

A surface that slides up over the screen for a short, focused job — usually choosing one thing —
and goes away when it is done.

```kata-specimen
sheet-picker
```

> Source: [`BottomSheet.tsx`](../../../src/features/train/components/BottomSheet.tsx), exported
> from the barrel; `ActionMenu` and `RankPickerSheet` are built on it. The full picker spec:
> [sheet-picker](../patterns/sheet-picker.md).

## 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. 0.35 black in light, 0.5 in dark, read from the theme |
| 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.control` on `surface.card` is 3.53:1 in light, so the grabber is visible on the sheet.

## Do and don't

```kata-specimen
sheet-selected
```

## Code

```tsx
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>
```
