Browse all of Kata

Bottom sheet

Source docs/design/components/bottom-sheet.mdMarkdown

On this page

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 rank

Choose a rank

White
One green stripe
Two green stripes
Green
Brown

Dark

New rank
Choose a rank

Choose a rank

White
One green stripe
Two green stripes
Green
Brown

Source: BottomSheet.tsx, exported from the barrel; ActionMenu and RankPickerSheet are 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.control on surface.card is 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>