Components · Overlays & menus
Drawer
A panel that slides in from an edge over a scrim — for filters, detail, or a secondary task that needs more room than a popover. It sits on a card surface with shadow-overlay; anchored to the right, only its left corners round. Build it on the native <dialog> so focus-trap, Escape, and the backdrop come for free.
Preview
Filters
Refine the list by status, owner, and date. Changes apply as you go.
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Drawer" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `drawer` spec (variants: drawer, drawer-overlay) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `drawer` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
┌─ drawer-medium (400px) ─────────┐
← viewport │ ┌─ drawer-header ─────────── ✕ │
│ │ Edit project │
│ └────────────────────────────────│
│ ┌─ drawer-body (scrolls) ───────│
│ │ [form fields] │
│ └────────────────────────────────│
│ ┌─ drawer-footer ────────────────│
│ │ [Cancel] [Save] │
│ └────────────────────────────────│
└────────────────────────────────────┘
drawer-overlay ← (click-outside closes, if overlay mode)
| State | Visual |
|---|---|
| Closed | Not rendered (or translateX(100%), offscreen) |
| Opening | Slides in + fades in per animation tokens |
| Open | Fully visible, focus trapped inside |
| Closing | Slides out + fades out per animation tokens |
| Loading (content) | Skeleton fills drawer body |
When to use
- Contextual detail panels (click a row to see full details without losing list context).
- Multi-field edit forms where navigating to a new page would lose the user's place.
- Filter panels that apply to a list or table.
- Mobile navigation (full-width overlay drawer as the primary nav).
When not to use
- Simple single-field edits — use a Popover instead.
- Confirmations ("Are you sure?") — use a Modal.
- Long multi-step wizards — use a full page or Modal.
Do
- Always include a close button (✕) in
drawer-header— don't rely only on Escape or overlay click. - Fix
drawer-footerat the bottom so action buttons are always visible without scrolling. - Use
drawer-widefor forms with 5+ fields — narrow forms create uncomfortable line lengths. - Restore focus to the trigger element when the drawer closes.
Don't
- Don't use
closeOnOverlay={false}unless the drawer contains a form where accidental close would destroy significant unsaved work. - Don't open a drawer from inside a drawer — flatten the interaction or use a multi-step form within one drawer.
- Don't use
drawer-full(sidebar replacement) in overlay mode — it's for push mode only.
- Title: noun phrase — "Edit profile", "Project details", "Filter options".
- Footer: primary action rightmost ("Save changes"), cancel leftmost ("Cancel").
- Body: use form component conventions for field layout — 16px internal padding matches
{spacing.page}.
Keyboard
| Key | Behavior |
|---|---|
Tab |
Cycle focus through interactive elements inside the drawer (focus trap) |
Shift + Tab |
Reverse cycle within the focus trap |
Escape |
Close the drawer, return focus to the trigger |
Accessibility
- ARIA role:
role="dialog"witharia-modal="true"in overlay mode;role="complementary"orrole="navigation"in push/full mode. aria-labelledby: points todrawer-header-titleelement.- Focus trap: required in overlay mode. On open: first interactive element inside or the close button. On close: trigger element.
- Push mode: no focus trap — page content remains accessible.
drawer-overlay:aria-hidden="true"on the blanket element.
Spec
Values and token references resolved straight from the Zetta spec.
drawer
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- 8px 0 0 8px
- shadow
- {elevation.shadow.overlay}
- padding
- {spacing.page}
- boxSizing
- content-box
- overflowY
- auto
- zIndex
- {elevation.z-index.modal}
- titleFontSize
- 18px
- titleFontWeight
- 600
- titleLineHeight
- 1.35
- bodyFontFamily
- Geist
- bodyFontSize
- 14px
- bodyColor
- {colors.body}
drawer-narrow
- width
- 320px
drawer-medium
- width
- 400px
drawer-wide
- width
- 560px
drawer-full
- width
- 240px
- borderRadius
- 0
- padding
- 0
drawer-left
- borderRadius
- 0 8px 8px 0
drawer-header
- display
- flex
- alignItems
- center
- justifyContent
- space-between
- paddingBottom
- {spacing.md}
- borderBottom
- 1px solid {colors.hairline}
- marginBottom
- {spacing.md}
drawer-header-title
- fontFamily
- Inter
- fontSize
- 18px
- fontWeight
- 600
- lineHeight
- 1.35
- textColor
- {colors.ink}
drawer-close
- iconSize
- {icons.sizeMap.md}
- iconColor
- {colors.muted}
- borderRadius
- {rounded.base}
- padding
- {spacing.xs}
- cursor
- pointer
drawer-close-hover
- backgroundColor
- {colors.primary-bg}
- iconColor
- {colors.primary}
drawer-close-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
drawer-body
- flex
- 1
- overflowY
- auto
drawer-footer
- borderTop
- 1px solid {colors.hairline}
- paddingTop
- {spacing.md}
- marginTop
- {spacing.md}
- display
- flex
- justifyContent
- flex-end
- gap
- {spacing.sm}
drawer-overlay
- backgroundColor
- {colors.overlay}
- position
- fixed
- inset
- 0
- zIndex
- {elevation.z-index.blanket}
drawer-push-layout
- transition
- margin-left {motion.duration-moderate} {motion.easing-default}
drawer-enter
- animation
- drawer-in {motion.duration-moderate} {motion.easing-enter}
- transform
- translateX(100%) → translateX(0)
drawer-enter-left
- transform
- translateX(-100%) → translateX(0)
drawer-exit
- animation
- drawer-out {motion.duration-base} {motion.easing-exit}
- transform
- translateX(0) → translateX(100%)
drawer-nested-offset
- zIndex
- +10
drawer-parent-dimmed
- opacity
- 0.7
- pointerEvents
- none
drawer-mobile
- width
- 100vw
- maxWidth
- 100vw
- borderRadius
- 0
Known gaps
- Multi-level drawer (more than 2 levels) — actively discouraged; no spec.
- Drawer resize handle (user-draggable width) — not yet specified.