Components · Overlays
Drawer
BetaA mobile bottom sheet.
- Source
components/ui/drawer.tsx - shadcn/ui Drawer
- Figma: not linked yet (Q13)
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
mdbreakpoint 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
| Variant | Use 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
| State | What changes | Token |
|---|---|---|
| Opening, closing | Slides up from the bottom edge, set by Vaul (500ms, its own easing) | none: Vaul's timing isn't a motion token |
| Dragging | The panel follows the finger; releasing past the threshold closes it | none |
| Open | Scrim over the page, panel on the modal layer | bg-foreground, opacity-(--opacity-overlay), z-(--z-modal) |
| Focus | Moves to the first focusable element; returns to the trigger on close | ring-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.
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
| Key | Does |
|---|---|
| Enter or Space on the trigger | Opens the drawer |
| Tab / Shift+Tab | Cycles through the drawer only; focus is trapped |
| Escape | Closes the drawer and returns focus to the trigger |
- Rendered as
role="dialog"witharia-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.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --background | bg-background | white | white |
| --foreground | bg-foreground text-foreground | neutral-950 | neutral-950 |
| --muted | bg-muted | neutral-100 | neutral-100 |
| --muted-foreground | text-muted-foreground | neutral-600 | neutral-600 |
- Layer
z-(--z-modal) - Layer
z-(--z-overlay) - Opacity
opacity-(--opacity-overlay) - Radius
rounded-full - Radius
rounded-b-lg rounded-t-lg - Type
font-semibold - Type
text-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