Designer view

Drawer

An edge-anchored panel that slides in from a viewport edge to surface contextual content alongside (or temporarily replacing) the underlying view. Distinct from Modal in two ways: positioning is edge-anchored rather than centered, and the modal/non-modal behaviour is a property rather than the defining trait. Common uses: filter panels, detail-view side-sheets, mobile navigation, settings flyouts.

When to use

Use

When contextual content should surface alongside (or temporarily replacing) the underlying view from a viewport edge — filter panels, detail-view side-sheets, mobile navigation, mobile bottom-sheet pickers. Drawer is preferred over Modal when the relationship to the underlying content matters and the user benefits from continued visual context.

Avoid

For blocking decisions or destructive confirmations — that is `Modal[role=alertdialog]`. For contextual content tied to a specific trigger — that is `Popover`. For non-blocking inline notifications — that is `Toast` or `Banner`. For a permanent column that is part of the layout — that is a layout primitive, not a Drawer.

Versus related

  • modal

    `Modal` always centres and is always modal; `Drawer` is edge-anchored and can be modal or non-modal. Drawer preserves spatial relationship to the underlying content (the page is still partly visible); Modal severs that relationship.

  • popover

    `Popover` is anchored to a *trigger element* and floats near it; `Drawer` is anchored to a *viewport edge*. Popover is always non-modal and dismissable on outside click without ceremony; Drawer can be modal and may require explicit dismissal. Use Popover for content the user reads and dismisses; Drawer for content the user *acts in*.

  • alert

    `Alert` is a non-blocking inline message announced via `aria-live`. Drawer is a navigable surface; Alert is a static message. The `Drawer[variant=navigation]` is similar to a SidebarNav, not to Alert.

Highlight
Fig 1.1 · Drawer · Designer view
Designer

Figma anatomy

Slot Figma type Hint
backdrop frame Full-bleed frame with semi-transparent fill; conditional on modal variant
container frame Edge-anchored auto-layout frame; size variant drives inline-size for left/right, block-size for top/bottom
header frame Auto-layout horizontal frame at the inline-start edge of the container
title text Heading text style; bound to a component property for content
close-button instance Icon button instance; bound to component slot
body frame Auto-layout vertical frame; min-block-size drives "comfortable" density
footer frame Auto-layout horizontal frame; right-aligned actions in LTR
handle rectangle 4×40px pill at the leading edge; visibility bound to "swipeable" property
Designer

Token usage per slot

backdrop
color
  • backgroundcolor.surface.scrim
container
spacing
  • paddingspacing.comfortable
radius
  • cornerradius.lg
color
  • backgroundcolor.surface.raised
  • bordercolor.border.subtle
elevation
  • shadowelevation.overlay
header
spacing
  • paddingspacing.compact
  • gapspacing.compact
title
color
  • foregroundcolor.text.primary
typography
  • sizetext.lg
  • weightweight.semibold
  • lineHeightleading.tight
close-button
spacing
  • paddingspacing.tight
radius
  • cornerradius.sm
color
  • foregroundcolor.text.muted
  • ringcolor.border.focus
body
spacing
  • paddingspacing.comfortable
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
  • lineHeightleading.normal
footer
spacing
  • paddingspacing.compact
  • gapspacing.compact
handle
radius
  • cornerradius.pill
color
  • backgroundcolor.border.subtle
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps modal / non-modal / navigation. Drives ARIA role.
SideVariantsidestart / end / top / bottom. Encoded as Variant for preview-time positioning review.
SizeVariantsizesm / md / lg / full. Drives container inline-size (left/right) or block-size (top/bottom).
DismissibleBooleandismissibleToggles close-button visibility plus Escape and backdrop-click handling. Some host libraries combine the three; canonical treats them as one switch.
SwipeableBooleanswipeableToggles handle visibility plus swipe-gesture wiring. Generally true on mobile breakpoints, false on desktop.
Has BackdropBooleanbackdropDerived in canon from `variant: modal` (true) vs `non-modal` (false); Figma may expose it as a separate Boolean for design preview.
TitleTexttitle
Has HeaderBooleanheader
BodyInstance SwapbodySwaps the body content slot (form, prose, list, custom layout).
Has FooterBooleanfooter
FooterInstance Swapfooter
Designer

Motion

TransitionDuration token
openmotion.duration.base
closemotion.duration.base
backdropmotion.duration.fast
dragSnapmotion.duration.fast
Easing
motion.easing.decelerate
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smAt and below, all drawer variants render as the `bottom` side with `size: full` regardless of authored `side` and `size` properties — narrow viewports do not afford left/right edge anchoring without crowding the underlying content. Navigation drawers may further degrade to a fullscreen overlay (`size: full`, suppressed handle).
breakpoint.mdAbove this width, all variants render as authored. The `navigation` variant typically pins as a permanent column on desktop (non-modal, `size: md` minimum); below `md` it collapses back to the modal-with-trigger pattern.
Both

Internationalisation

RTL · mirroring

The `side` property is logical, not physical: `start` slides from the inline-start edge (visual right in RTL), `end` from the inline-end (visual left in RTL). Implemented via `inset-inline-start` / `inset-inline-end`, not `left` / `right`. Top and bottom sides are direction-neutral. Close-button positioning inside the header follows the existing logical pattern (close on inline-end). Swipe-to-dismiss gesture direction reverses for `start` and `end` sides under RTL.

Text expansion

Drawer width is sized in CSS (`size: sm | md | lg | full`); long titles and labels grow naturally within the inline-size. For `size: sm`, German and Russian titles risk wrap or truncation — `size: md` is the safer canonical default in long-text locales. Body content scrolls; footer buttons may wrap to two rows under heavy expansion.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

modalnon-modalnavigation

Properties

The same component, parameterised.

PropertyType
sidestart | end | top | bottom
sizesm | md | lg | full
dismissibleboolean
swipeableboolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
focus-visiblehover
data
closedopeningopenclosingdragging
Both

State transitions

FromToTrigger
closedopeningUser activates the trigger that owns the drawer (button click, link activation, programmatic `open()`). For modal drawers the previously-focused element is captured for restoration on close; for non-modal, focus does not move automatically.
openingopenThe slide-in animation completes (or, under `prefers-reduced-motion: reduce`, immediately after `closed → opening`). Modal: focus moves into the drawer, focus trap engages, siblings become `inert`. Non-modal: drawer is ready, focus stays where it was.
openclosingUser dismisses via the close button, Escape (when `dismissible: true`), backdrop click (modal + dismissible), or swipe gesture (when `swipeable: true`); or the primary action commits and programmatically requests close.
closingclosedThe slide-out animation completes (or immediately under reduced motion). Modal: drawer is removed from the AT tree, `inert` is released, focus restores to the captured trigger. Non-modal: drawer is removed; focus stays where it was.
opendraggingUser initiates a swipe gesture on the handle or container edge (`swipeable: true`). The drawer follows the pointer position with reduced damping; release-velocity above threshold triggers `dragging → closing`, below resets to `dragging → open`.
draggingopenUser releases the swipe with insufficient velocity / distance to dismiss; drawer animates back to its open position.
draggingclosingUser releases the swipe with sufficient velocity / distance to dismiss; drawer animates the rest of the way out via the standard close path.
Both

Figma↔Code mismatches

  1. 01
    Figma

    A drawer drawn as a static side panel that's just always there

    Code

    A portal-mounted panel with focus trap, escape handling, slide animation, and `inert` toggling

    Consequence

    The Figma artifact captures the visual end-state but encodes none of the open/close lifecycle, the modality, or the focus contract. Designers approximating the canonical drawer from the Figma file may not realise modal drawers need `inert`, escape handling, and focus trap; developers may ship something that looks like a drawer but escapes attention semantics.

    Correct

    Document the open/close lifecycle and modality contract in the canonical reference. The Figma file captures visual states (closed, opening, open, dragging); the canonical reference and a11y notes document the portal mount, focus trap, escape, and `inert` for modal drawers.

  2. 02
    Figma

    Modal drawer and non-modal drawer drawn as one variant set

    Code

    Two distinct accessibility contracts (focus trap + inert vs none) driven by the variant prop

    Consequence

    Designers may pick "drawer" without realising the modality choice determines whether keyboard users can tab past the drawer; the canonical contract for `role` (`dialog` vs `region`) differs between the two. Developers shipping non-modal-styled-as-modal lock keyboard focus inside a panel that looks dismissable just by clicking outside.

    Correct

    Treat modal vs non-modal as a structural variant (separate ARIA role, separate keyboard contract). Designers and developers both consult the canon to confirm which variant they're using; the Figma component carries a Variant property that surfaces both.

  3. 03
    Figma

    Swipe-to-dismiss drawn as a static "drag handle" with no animation

    Code

    A pointer-event-driven gesture handler tracking velocity and distance, with kinematic close-or-snap-back

    Consequence

    Designers see a static handle and may assume it's just a visual affordance; developers either ship without swipe support (touch-input feels broken) or invent the gesture interaction from scratch. Animation curves and dismiss thresholds are reinvented per implementation.

    Correct

    Document swipe-to-dismiss as a `swipeable: true` property with explicit canonical states (`dragging`) and transitions. Specify threshold conventions in the canonical reference (e.g. "release velocity > 800px/s OR distance > 30% closes").

  4. 04
    Figma

    Side `start` vs `end` drawn as two separate components

    Code

    A single component with a `side` property toggling logical positioning

    Consequence

    Designers and developers count drawers differently — designers see four components (left, right, top, bottom drawers); developers see one with a property. Variant explosion (4 sides × 3 modal-modes × 4 sizes = 48 variants) makes the Figma file unmaintainable.

    Correct

    Model `side` as a property, not as separate components. Use Figma's Variant property type for the four side values; the anatomy is shared across all four.

Designer

Common mistakes

#drawer-no-focus-trap-on-modal

Modal drawer with no focus trap

Problem

The drawer is styled to feel modal (backdrop, scrim) but pressing Tab from the last focusable inside the drawer moves focus to the next element in the page beneath the backdrop. Visually obscured but technically focused.

Fix

Implement a focus trap when `modal: true`. Modern primitives (Radix Dialog, React Aria Modal, vaul) include this; never roll your own without `inert` on the rest of the document.

#drawer-no-focus-restore

Focus is lost after the drawer closes

Problem

After dismissal, focus lands on `<body>`. Keyboard and screen-reader users have to re-orient from the top of the page.

Fix

Capture the previously-focused element on open (modal variants). Restore focus to it on close. If the trigger no longer exists, focus a stable landmark (page heading, parent toolbar).

#drawer-swipe-without-keyboard-equivalent

Swipe-to-dismiss with no keyboard equivalent

Problem

Mobile drawers are dismissable via swipe but keyboard users on desktop have no equivalent. Either the drawer cannot be closed from the keyboard at all, or the close button is missing on the assumption "users will swipe".

Fix

Every dismissible drawer carries a visible close-button slot reachable by keyboard. Escape dismisses when `dismissible: true` regardless of input modality. Swipe is an additive convenience, not a replacement.

#drawer-side-not-rtl-aware

Side `start` hard-coded to `left`

Problem

The drawer slides from the left in LTR — and also in RTL, breaking RTL users' expectation that "start" means visual right. Implementations using `left: 0` or `transform: translateX(-100%)` hard-code direction.

Fix

Use logical properties: `inset-inline-start: 0`, `transform: translateX(calc(-1 * 100%))` only when paired with `:dir(rtl)` overrides, or better, use logical-property-aware transforms via a CSS variable. The drawer's `side: start` slides from inline-start in any direction.

#drawer-stacking-with-modal

Drawer opened on top of an existing Modal

Problem

Both surfaces install their own focus traps and `inert` handling. The two systems fight: focus may bounce between the modal and the drawer; assistive tech reads stale content from the now-inert modal beneath.

Fix

Forbid stacking by canon: a Drawer and a Modal cannot be open simultaneously. Either dismiss the modal before opening the drawer, or model the drawer's content as a step inside the modal flow. Stacking is a redesign signal, not a layering concern.

Accessibility hints
Slot Accessibility hint
backdrop Backdrop is presentational; do not put `role` on it. Clicking the backdrop dismisses the drawer when `dismissible: true`. Keyboard users always have an explicit close affordance because they cannot click the backdrop.
container For modal drawers, apply `role="dialog"` and `aria-modal="true"`, with focus trap and `inert` on siblings. For non-modal drawers, `role="region"` with `aria-labelledby` pointing at the title is the canonical choice — focus may escape the drawer naturally. Always label via `aria-labelledby` regardless of modality.
header Header is a layout region, not a heading. Heading semantics live on the title element inside.
title Use a real heading element of an appropriate level. The drawer's container references this element via `aria-labelledby`. Hidden titles (visually-hidden but accessible) are valid for drawers that do not visually display a title.
close-button Provide an accessible name ("Close" or "Close filters"). The Escape key must do the same thing as clicking this button when `dismissible: true`.
body Body retains its native semantics (forms stay forms, lists stay lists). For scroll-on-overflow, ensure the scrollable region is keyboard-reachable (`tabindex="0"` on the scroll container) so keyboard users can scroll without an interactive child.
footer Buttons keep native semantics. Document the default focus target (usually the primary commit button); destructive commits should default focus to the cancel action.
handle Decorative when only providing a visual cue. If the handle also accepts pointer drag for resize, expose it as `role="separator"` with `aria-orientation` and ARIA range values, and provide keyboard equivalents (ArrowKeys to resize).