Home/Patterns/Bottom sheet

Bottom sheet

The mobile-first dialog: anchored to the bottom of the viewport on phone, centred as a modal on desktop. Header, scrollable body, sticky footer. Replaces the bespoke .sheet / .more-sheet / .modal mobile variants shipped by Admin, Parent, Student, Gold, and Owner.

Stable .x-bottom-sheet 3 compositions

Shell at every breakpoint

Bottom-anchored on phone, centred on desktop. Greyscale wireframes — structure only.

Mobile
SM
Tablet
MD
Desktop
LG

Live examples

Where to see this shell in action — reference docs, cookbook recipes, and live demos.

COMPOSITION 01

Detail sheet

The default: tap a row card and it opens. Headline, body content, and a sticky footer with the primary action. Header carries a close button and a drag handle for vertical-swipe dismiss.

Use whenThe operator drilled into a single record and you need to show the full detail without leaving the list. The sheet keeps context — closing returns to the scroll position.

COMPOSITION 02

Action sheet

A list of destructive / lateral actions surfaced from a "…" button. Each row is a tap target with an icon tile on the left.

Use whenThe operator triggered a per-row menu (the kebab) and you need to offer 2–8 actions. Past 8, use a dedicated screen.

COMPOSITION 03

Desktop dialog

≥ 720 px viewport, the sheet centres as a 520 px-wide dialog with rounded top & bottom corners. Same parts — only the entrance is different (fade + lift, not slide-up).

Use whenThe same React component renders this automatically. Components don't fork by viewport; one source of truth, two layouts.

Parts

PropertyValueNotes
scrimink @ 55% opacity200 ms fade in / out
radius (phone)18 px top, 0 bottomAnchored to viewport edge
radius (desktop)14 px all cornersFree-floating dialog ≥ 720 px
max height90 vh phone, 80 vh desktopBody scrolls when over
grab handle38 × 4 px, --border-strongPhone only; hidden on desktop
headerflex, padding 10–18, border-bottom subtleSticky — header stays visible while body scrolls
footersafe-area padding, border-top subtleButtons stretch to fill; primary on the right
entrance (phone)translateY 100% → 0260 ms cubic-bezier(0.2,0,0.1,1)
entrance (desktop)translateY 20 → 0, opacity 0 → 1200 ms ease-out

Shared rules

Do

Close on scrim tap, Escape key, drag-down past a threshold, and explicit close button. Each is the only one some users know.

Don't

Stack sheets. If a sheet opens another, you've designed a wizard — use a stepper instead.

Do

Use a sheet for any modal that's > 1 viewport tall on phone. The internal scroll inside the sheet is gentler than full-page scroll under chrome.

Don't

Put navigation links in a sheet. Sheets are for the current record / current action — not a substitute for the sidebar.

Do

Trap focus inside the sheet when open. Tab cycles through interactive children only; Shift+Tab from the first wraps to the last.

Don't

Animate close as a slow slide. 200 ms or less — operators want the canvas back fast.

Accessibility

  • The sheet is a <div role="dialog" aria-modal="true" aria-labelledby="bs-title">. The header H3 carries that ID.
  • Open: move focus to the close button (or the first interactive child). Close: restore focus to the element that opened the sheet.
  • Inert the rest of the page (inert attribute on the app container) so AT can't reach the background.
  • Drag handle is a visual cue, not interactive — keyboard users use Escape or the close button.

Related

  • Modal & sheet — the broader modal family (alert dialog, confirm, etc.).
  • Row card — the primary opener for detail sheets.
  • Bottom tabs — the "More" tab opens a sheet of overflow destinations.
  • Detail view — when the detail is too big for a sheet, use a full screen.