Drawer
A drawer is a panel that slides in from an edge of the viewport, typically used for navigation menus, filters, or supplementary content. It overlays the main content and can be dismissed by the user. The component renders conditionally based on its open state and uses role="dialog" with aria-modal="true" for proper screen reader behavior.
The side prop indicates which edge the drawer enters from via a data-side attribute, allowing consumers to apply appropriate positioning and animation styles. The drawer supports keyboard dismissal with the Escape key and provides a tabindex="-1" to allow programmatic focus management.
Example
Show demo markup
<div class="drawer" role="dialog" aria-label="Drawer"><p>Drawer content</p></div>
Usage example
<Drawer label="Navigation" bind:open side="left">
<nav aria-label="Main">…</nav>
</Drawer>
Show Svelte source
// In your Svelte component:
import Drawer from "lily-design-system-svelte-headless/components/Drawer/Drawer.svelte";
<Drawer>
<!-- content -->
</Drawer>
Usage
Mobile navigation drawer sliding in from the left:
<button onclick={() => (navOpen = true)}>Menu</button>
<Drawer label="Main navigation" open={navOpen} side="left">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<button onclick={() => (navOpen = false)}>Close</button>
</Drawer>
Filter panel sliding in from the right:
<Drawer label="Search filters" open={showFilters} side="right">
<form>
<fieldset>
<legend>Date range</legend>
<input type="date" name="from" />
<input type="date" name="to" />
</fieldset>
<button type="submit">Apply filters</button>
</form>
</Drawer>
Props
open: boolean (default: false) -- whether the drawer is visible, two-way bindable via open
label: string (required) -- accessible name for the drawer
side: "left" | "right" | "top" | "bottom" (default: "left") -- which edge the drawer enters from
children: slot (required) -- drawer content
Keyboard Interactions
- Escape: closes the drawer by setting
open to false
ARIA
role="dialog" -- identifies the drawer as a dialog for assistive technology
aria-label={label} -- provides an accessible name for the dialog
aria-modal="true" -- indicates the drawer is modal, restricting interaction to its contents
data-side={side} -- data attribute for consumer CSS styling based on drawer position
When to Use
- Use for a panel that slides in from the edge of the screen to show secondary content or navigation without leaving the page.
- Use for navigation menus, filters, or supplementary content that should overlay the main view temporarily.
- Use when the content is contextual and temporary, not a primary destination.
- Use for responsive layouts where side navigation collapses into a drawer on smaller screens.
When Not to Use
- Do not use for modal confirmations or urgent messages -- use Dialog or AlertDialog instead.
- Do not use for tooltip-style content -- use Popover instead.
- Do not use when the overlay should behave like a bottom sheet on mobile devices -- use Sheet instead.
- Do not use for persistent side navigation that should always be visible -- use Sidebar instead.
Headless
This headless component provides a <div> with role="dialog", aria-modal="true", accessible labeling via aria-label, a data-side attribute for positioning, keyboard dismissal via Escape, and two-way binding on open. The consumer provides all visual styling, animations, backdrop, and positioning CSS.
Styles
The consumer provides all CSS styling. The component renders with a .drawer class for targeting. No default styles are included — this is a fully headless component.
Testing
- Verify the component renders a
<div> element with class drawer
- Verify role="dialog"` -- identifies the drawer as a dialog for assistive technology
- Verify aria-label={label}` -- provides an accessible name for the dialog
- Verify keyboard interactions work correctly
- Verify pass-through attributes are applied
Advice
- Designers: Include a visible close button and consider adding a semi-transparent backdrop to indicate the main content is temporarily inaccessible. Animate the slide-in transition for a smooth experience.
- Developers: Trap focus within the drawer when it is open and return focus to the trigger element on close. Use the
data-side attribute to apply directional CSS transitions.
Implementation Notes
- Conditionally renders a
<div> with role="dialog" only when open is true
- Supports two-way binding on the
open prop for two-way state binding
- Sets
aria-modal="true" to indicate the drawer is a modal dialog
- Exposes
data-side attribute for consumer CSS positioning (left, right, top, bottom)
- Sets
tabindex="-1" to allow the drawer to receive focus programmatically
- Escape key handler sets
open to false
Related components
sheet — a panel that slides in from a screen edge as an overlay
slide-out-drawer — a drawer that slides out from the side of the page
sidebar — a side panel for navigation or supplementary content
References
- WAI-ARIA Dialog Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
Canonical contract
Generated from this component's canonical metadata — the machine-checked source the implementations are held to.
Metadata
- Component: drawer
- PascalCase: Drawer
- Description: a panel that slides in from the edge of the screen
- Status: stable — exercised in composed page flows under e2e and axe, beyond the per-component checks
- HTML tag: <div>
- CSS class: .drawer
- Interactive: yes
ARIA
role="dialog" -- identifies the drawer as a dialog for assistive technology aria-label={label} -- provides an accessible name for the dialog aria-modal="true" -- indicates the drawer is modal, restricting interaction to its contents data-side={side} -- data attribute for consumer CSS styling based on drawer position
Keyboard
- Escape: closes the drawer by setting
open to false
Props
open: boolean (default: false) -- whether the drawer is visible, two-way bindable via open label: string (required) -- accessible name for the drawer side: "left" | "right" | "top" | "bottom" (default: "left") -- which edge the drawer enters from children: slot (required) -- drawer content