Zetta v1.6

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.

Prompt for your AI
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-footer at the bottom so action buttons are always visible without scrolling.
  • Use drawer-wide for 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" with aria-modal="true" in overlay mode; role="complementary" or role="navigation" in push/full mode.
  • aria-labelledby: points to drawer-header-title element.
  • 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.