Start typing to search components and docs…
select navigate ESC close

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:

ActionBehavior
Click backdropCloses drawer
Press EscapeCloses 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-label set from the label prop (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 })
  • Escape key 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

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>