Overview
Drawer is a CORE-OWNED reusable slide-in overlay primitive in src/core/components/primitives/Drawer.astro (D1). It provides a focus-trapped panel with a dismissible backdrop, ESC-to-close, scroll lock, and reduced-motion support.
The consumer owns the state — the parent element provides a boolean variable that controls the drawer’s open/close state. The binding prop tells Drawer which variable to bind to.
Who this is for
- Implementors using Drawer for mobile menus, filter panels, or side navigation
- Developers building new overlay-based components on top of the Drawer primitive
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Element ID for the drawer panel |
position | "left" | "right" | "full-width" | "right" | Slide-in direction and panel sizing |
binding | string | "open" | Name of the boolean variable controlling open/close state on the parent element |
label | string | — | Accessible label for the dialog panel (aria-label) |
class | string | — | Additional CSS classes on the drawer panel element |
Architecture
Drawer renders two sibling DOM elements:
- Backdrop — a fixed overlay covering the full viewport using
--st-color-nav-mobile-overlayat 50% alpha with click-to-close behavior. Markedaria-hidden="true"so screen readers ignore it. - Panel — the drawer content area, positioned fixed with
overflow-y: auto,overscroll-contain, and--st-color-nav-mobile-bgfor the mobile navigation surface.
Both elements are hidden by default and revealed by the vanilla JS controller to prevent a flash of content before initialization.
Position variants
| Position | Placement | Width | Transition |
|---|---|---|---|
left | Left edge | min(20rem, 85vw) | translateX slide |
right | Right edge | min(20rem, 85vw) | translateX slide |
full-width | Covers full viewport | 100% | Opacity fade |
The backdrop and panel use the semantic layer contract: z-modal-backdrop for the backdrop and --st-layer-modal-surface for the panel, ensuring the panel sits deterministically above the backdrop without local arithmetic.
Behavior
Opening and closing
The drawer opens when the bound variable becomes true and closes when it becomes false. Closing can be triggered by:
- Clicking the backdrop overlay
- Pressing the Escape key (via a keyboard event listener)
- Any consumer-provided close button that sets the bound variable to
false
Focus trapping and scroll lock
When open, the vanilla JS controller on the panel element:
- Traps focus inside the panel — Tab and Shift+Tab cycle through focusable elements within the drawer only.
- Locks scroll on the body — the page behind the drawer cannot be scrolled while the drawer is open.
Focus trapping and scroll lock are implemented in vanilla TypeScript.
Transitions
- Left/right positions use
translateXslide animations with--pt-duration-normalduration and--pt-ease-standardeasing. - Full-width position uses an opacity fade with the same timing tokens.
- Backdrop always uses an opacity fade transition.
- All transitions are implemented via CSS transition classes defined in a scoped
<style>block.
Reduced motion
All transitions are disabled when prefers-reduced-motion: reduce is active. The @media query sets transition: none on all transition classes, so the drawer appears and disappears instantly.
Usage
The consumer must wrap the Drawer in an element with data-slot that defines the boolean variable referenced by binding:
<div data-slot="drawer-host">
<button data-action="open-drawer">Open</button>
<Drawer
id="my-drawer"
binding="drawerOpen"
label="Navigation menu"
position="left"
>
<!-- drawer content -->
<button data-action="close-drawer">Close</button>
</Drawer>
</div>
Note: The
bindingprop identifies the state variable managed by the vanilla JS controller. The default is"open".
Accessibility
role="dialog"andaria-modal="true"on the panel element identify it as a modal dialog to assistive technologies.aria-label(from thelabelprop) provides an accessible name for the dialog. Always provide a meaningful label.- Focus is trapped inside the panel when open via vanilla JS focus trapping, preventing keyboard users from tabbing to elements behind the overlay.
- Escape key closes the drawer, following the standard modal dismissal pattern.
- The backdrop is marked
aria-hidden="true"so it is not announced by screen readers.
CSS API
BEM root class: drawer
| Class | Element |
|---|---|
.drawer | Panel root |
.drawer--left | Left-positioned panel variant |
.drawer--right | Right-positioned panel variant |
.drawer--full-width | Full-width panel variant |
.drawer-backdrop | Backdrop overlay |
Current consumers
- MobileMenu — uses
binding="mobileOpen"withposition="left"for the responsive navigation drawer. - Showcase navigation demo page — demonstrates drawer positioning variants.
Vanilla JS implementation
Drawer uses vanilla TypeScript for focus trapping, scroll lock, keyboard handling, and CSS transitions. No framework dependencies are required.