Skip to content
SuperCoach

Components · Overlays

Drawer

Beta

A mobile bottom sheet.

A bottom sheet for phones, built on Vaul. The coach can drag it down to close it.

When to use

  • For a quick task on a phone, done with one thumb: log a set, send a quick check-in, pick a meal swap.
  • When the task is short and the coach goes straight back to the screen behind it.

When not to use

  • On desktop: use a Dialog for a short task or a Sheet for a side panel. A common pattern renders a dialog from the md breakpoint up and a drawer below it, with the same content.
  • For long forms: use a page. A drawer is at most 80% of the screen's height.
  • To confirm a destructive action: use an Alert dialog, which can't be swiped away by accident.
  • For menus of actions on a row: use a dropdown menu.

Anatomy

Open the drawer above to see its parts: the scrim over the page; the panel rising from the bottom with rounded top corners; the handle (a short bar that shows it can be dragged); a header with title and description, centered; the content; and a footer with full-width actions, primary first. There is no close button: the coach drags it down, taps the scrim, or uses Cancel.

Variants

VariantUse for
Form (log a set)Two or three fields and a save button
Choice (quick check-in)Picking one of a few options, then one action

Vaul also supports direction="top", "left" and "right". Don't use them: side panels are a Sheet.

States

StateWhat changesToken
Opening, closingSlides up from the bottom edge, set by Vaul (500ms, its own easing)none: Vaul's timing isn't a motion token
DraggingThe panel follows the finger; releasing past the threshold closes itnone
OpenScrim over the page, panel on the modal layerbg-foreground, opacity-(--opacity-overlay), z-(--z-modal)
FocusMoves to the first focusable element; returns to the trigger on closering-ring/50

The scrim color is a default: there is no overlay color token (QUESTIONS Q6). Vaul's slide is longer than --duration-slow (300ms) and doesn't respond to reduced motion.

Usage guidelines

  • Put the primary action at the bottom, full width, where the thumb is. Use size="lg" buttons.
  • Keep content narrow on wide screens (max-w-sm, centered), as in the examples.
  • Set dismissible={false} only when closing would lose work, and then provide a Cancel button.
  • One drawer at a time. Don't open a drawer from a drawer.
Do.Full-width, large buttons at the bottom: easy to reach with a thumb.
Don't.Small buttons in a corner. Hard to hit on a phone, and Save doesn't say what it saves.

Content guidelines

  • Title: the object, specific enough to be sure what you're logging: "Back squat: set 3", "Check in with Lina".
  • Description: the useful context, such as the last result: "Last time: 80 kg for 8 reps".
  • Buttons: a verb and an object, "Save set", "Send check-in". Arabic: "حفظ المجموعة", "إرسال المتابعة".

Accessibility

KeyDoes
Enter or Space on the triggerOpens the drawer
Tab / Shift+TabCycles through the drawer only; focus is trapped
EscapeCloses the drawer and returns focus to the trigger
  • Rendered as role="dialog" with aria-modal, labelled by the title and described by the description. A screen reader announces "Back squat: set 3, dialog, Last time: 80 kg for 8 reps."
  • Dragging is never the only way to close it: Escape, tapping the scrim and the footer buttons all work (WCAG 2.5.7).
  • The handle is decorative and not focusable.
  • Always include a DrawerTitle, even if visually hidden.

RTL

The drawer rises from the bottom in both directions, so its position doesn't change. Text and fields inside follow the direction. The header is centered on phones; from the md breakpoint it aligns to the start.

Tokens used

Read from components/ui/drawer.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.

Semantic color tokens used by this component
TokenUtilitiesSuperCoachCoach
--backgroundbg-backgroundwhitewhite
--foregroundbg-foreground text-foregroundneutral-950neutral-950
--mutedbg-mutedneutral-100neutral-100
--muted-foregroundtext-muted-foregroundneutral-600neutral-600
  • Layerz-(--z-modal)
  • Layerz-(--z-overlay)
  • Opacityopacity-(--opacity-overlay)
  • Radiusrounded-full
  • Radiusrounded-b-lg rounded-t-lg
  • Typefont-semibold
  • Typetext-sm

Props

<Drawer>

Also takes every prop of Radix Drawer.Root

No props of its own.

<DrawerTrigger>

Also takes every prop of Radix Drawer.Trigger

No props of its own.

<DrawerClose>

Also takes every prop of Radix Drawer.Close

No props of its own.

<DrawerContent>

Also takes every prop of Radix Drawer.Content

No props of its own.

<DrawerHeader>

Also takes every prop of <div>

No props of its own.

<DrawerFooter>

Also takes every prop of <div>

No props of its own.

<DrawerTitle>

Also takes every prop of Radix Drawer.Title

No props of its own.

<DrawerDescription>

Also takes every prop of Radix Drawer.Description

No props of its own.

Drawer takes Vaul's root props, including direction, dismissible, snapPoints and onOpenChange.

Replaces in coach-admin

From the read-only audit of coach-admin: 24 style values, 25 uses.

coach-admin's bottom sheet becomes this component. Note that coach-admin's Drawer components are side panels; they become a Sheet.

Classes and selectors

  • .bottom-sheet4
  • .bottom-sheet__layer3
  • .bottom-sheet__scrim2
  • .bottom-sheet__content2
  • .bottom-sheet__header2
  • .bottom-sheet__grabber1

Files

  • src/reusableComponents/bottom-sheet/bottom-sheet.scss26

The inventory lists the first 12 locations of each value, so file counts are a lower bound.

SCSS variables in these styles

  • $spacing-xsnot a color: spacing, size or layout
  • $spacing-smnot a color: spacing, size or layout
  • $min-tap-target-sizenot a color: spacing, size or layout
  • $subtitle-text-color→ --muted-foreground
  • $card-surface-bgnot a color: spacing, size or layout
  • $border-strong-color→ --border-strong
  • $sheet-scrim-colornot a color: spacing, size or layout
  • $spacing-mdnot a color: spacing, size or layout
  • $primary-color→ --primary
  • $border-color→ --border
  • $sheet-shadownot a color: spacing, size or layout
  • $z-index-drawernot a color: spacing, size or layout

Sheet, Dialog, Layout.