Drawer
Slide-in side panel built on top of native <dialog> semantics. Used for mobile navigation in this docs site, but works as a general-purpose drawer for filters, settings, or off-canvas menus.
The drawer is CSS-driven with a small script that handles open/close, focus management, body scroll lock, and the Escape key. No Vue or framework runtime required — just the Drawer.astro component (or copy the HTML recipe).
Live demo
Default (Medium, Left-aligned)
---
import Drawer from 'spoko-design-system/components/Drawer.astro';
---<button type="button" class="btn-primary btn-normal" aria-controls="demo-drawer" aria-expanded="false">
Open drawer
</button><Drawer id="demo-drawer" label="Settings & Navigation">
<div class="p-6 flex flex-col gap-6">
<h3 class="text-xl font-textbold mb-4">Navigation</h3>
<ul class="flex flex-col gap-2">
<li><a href="#" data-drawer-close>Dashboard</a></li>
<li><a href="#" data-drawer-close>Products</a></li>
<li><a href="#" data-drawer-close>Settings</a></li>
</ul>
<button type="button" class="btn-secondary btn-sm" data-drawer-close>Close</button>
</div>
</Drawer>Small Width
<button type="button" class="btn-primary btn-normal" aria-controls="small-drawer" aria-expanded="false">
Open small drawer
</button><Drawer id="small-drawer" label="Quick actions">
<div class="p-4 flex flex-col gap-4">
<h3 class="text-lg font-textbold">Quick Actions</h3>
<button class="btn-primary btn-sm w-full">Action 1</button>
<button class="btn-secondary btn-sm w-full" data-drawer-close>Close</button>
</div>
</Drawer>Large Width
<button type="button" class="btn-primary btn-normal" aria-controls="large-drawer" aria-expanded="false">
Open large drawer
</button><Drawer id="large-drawer" label="Detailed panel">
<div class="p-6 flex flex-col gap-6">
<h3 class="text-xl font-textbold">Order Details</h3>
<!-- ... detailed content ... -->
<button type="button" class="btn-secondary w-full" data-drawer-close>Close</button>
</div>
</Drawer>Right-Aligned
<button type="button" class="btn-primary btn-normal" aria-controls="right-drawer" aria-expanded="false">
Open right drawer
</button><Drawer id="right-drawer" label="Side panel" position="right">
<div class="p-6 flex flex-col gap-4">
<h3 class="text-xl font-textbold">Right Panel</h3>
<p>This drawer opens from the right edge.</p>
<button type="button" class="btn-secondary w-full" data-drawer-close>Close</button>
</div>
</Drawer>Variants
Width Variants
Customize drawer width by overriding the .sds-drawer-panel width via CSS:
/* Small (narrow sidebar/quick actions) */
#my-drawer .sds-drawer-panel {
width: 14rem; /* 224px */
}
/* Medium (default navigation) */
#my-drawer .sds-drawer-panel {
width: 18rem; /* 288px */
}
/* Large (detailed panel with forms) */
#my-drawer .sds-drawer-panel {
width: 24rem; /* 384px */
}
Position: Right Side
Pass position="right" to slide from the right edge:
<Drawer id="my-drawer" position="right">…</Drawer>
For plain HTML, set data-position="right" on the .sds-drawer element — the built-in stylesheet handles the rest.
Wiring triggers
The drawer auto-discovers its trigger via the aria-controls attribute. Any element with aria-controls="<drawer-id>" opens the matching drawer on click. The script also manages aria-expanded for you.
<!-- Trigger: any focusable element with aria-controls pointing at the drawer id -->
<button type="button" aria-controls="nav-drawer" aria-expanded="false">
Open menu
</button>
<!-- Drawer with matching id -->
<Drawer id="nav-drawer">…</Drawer>
Closing the drawer
Three built-in ways to close — no extra wiring needed:
| Action | Behavior |
|---|---|
| Click backdrop | Closes drawer |
Press Escape | Closes drawer (when open) |
Click [data-drawer-close] | Closes drawer (use on any element inside the panel) |
Add data-drawer-close to any close button, link, or icon inside the drawer:
<Drawer id="nav-drawer">
<button type="button" data-drawer-close aria-label="Close menu">×</button>
<a href="/profile" data-drawer-close>Profile</a>
</Drawer>
Accessibility
The drawer follows the WAI-ARIA modal dialog pattern:
- Panel uses
role="dialog"+aria-modal="true" aria-labelset from thelabelprop (defaults to"Navigation")- Focus moves to the panel on open, and returns to the trigger on close
- Body scroll is locked while open (
body.drawer-open { overflow: hidden }) Escapekey dismisses the drawer
Always pass a descriptive label when the drawer is not the main navigation — screen readers announce it as the dialog name.
HTML recipe
The drawer is also available as a pure HTML + CSS pattern. Copy this into any framework or plain HTML page that loads the SDS stylesheet:
<button type="button" aria-controls="my-drawer" aria-expanded="false">
Open
</button>
<div id="my-drawer" class="sds-drawer" hidden>
<div class="sds-drawer-backdrop" data-drawer-close></div>
<aside
class="sds-drawer-panel"
role="dialog"
aria-modal="true"
aria-label="My drawer"
tabindex="-1"
data-drawer-panel
>
<!-- Drawer content here -->
<button type="button" data-drawer-close>Close</button>
</aside>
</div>
You also need the open/close logic. If you’re not using the Astro component, import the helper directly:
import { initDrawer } from 'spoko-design-system/src/scripts/drawer';
initDrawer('my-drawer');
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id
*
| string | — | Unique id used by triggers (aria-controls) and the open/close script |
label | string | 'Navigation' | aria-label for the dialog — describes the drawer to screen readers |
position | 'left' | 'right' | 'left' | Edge the drawer slides in from |
Use Cases
Navigation Menu
Typical mobile navigation with links. Close drawer on link click via data-drawer-close:
<Drawer id="nav-drawer" label="Navigation">
<nav class="p-6 flex flex-col gap-2">
<a href="/" data-drawer-close class="text-blue-medium hover:underline">Home</a>
<a href="/about" data-drawer-close class="text-blue-medium hover:underline">About</a>
<a href="/products" data-drawer-close class="text-blue-medium hover:underline">Products</a>
<a href="/contact" data-drawer-close class="text-blue-medium hover:underline">Contact</a>
</nav>
</Drawer>
Filters Panel
Form inputs for filtering product lists or search results:
<Drawer id="filters" label="Product Filters">
<div class="p-6 flex flex-col gap-4">
<div>
<label class="block text-sm font-textbold mb-2">Price Range</label>
<input type="range" class="w-full" min="0" max="1000" />
</div>
<div>
<label class="block text-sm font-textbold mb-2">Category</label>
<select class="w-full input-standard">
<option>All</option>
<option>Electronics</option>
<option>Clothing</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label class="flex gap-2 cursor-pointer">
<input type="checkbox" /> In Stock
</label>
<label class="flex gap-2 cursor-pointer">
<input type="checkbox" /> On Sale
</label>
</div>
<button class="btn-primary w-full" data-drawer-close>Apply Filters</button>
</div>
</Drawer>
Settings Panel
User preferences and toggles:
<Drawer id="settings" label="Settings">
<div class="p-6 flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-sm">Notifications</span>
<input type="checkbox" checked />
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Dark Mode</span>
<input type="checkbox" />
</div>
<hr class="border-slate-200" />
<button class="btn-secondary w-full" data-drawer-close>Close</button>
</div>
</Drawer>
Slot
The default slot renders inside the slide-in panel. Style it however you want — the panel itself only provides the positioning, sizing, transition, and scroll container.
<Drawer id="filters" label="Product filters">
<div class="p-6 flex flex-col gap-4">
<h2 class="text-lg font-textbold">Filters</h2>
<!-- your form here -->
</div>
</Drawer>