Drawer
Side panel using the native HTML <dialog> element. Slides in from the left or right.
Width comes from --af-drawer-width (default 20rem), capped at 90vw. That is not --af-sidebar-width — the latter sizes in-flow App shell and Sidebar.
Basic drawer
Section titled “Basic drawer”Click the button to open a real drawer. ESC, the close control, or the backdrop dismiss it.
<button class="af-btn"
onclick="document.getElementById('my-drawer').showModal()">Open Drawer</button>
<dialog class="af-drawer" id="my-drawer"
onclick="if (event.target === this) this.close()">
<div class="af-drawer-header">
<h3 class="af-drawer-title">Navigation</h3>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-drawer-body">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a href="#" class="af-side-nav-link" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a href="#" class="af-side-nav-link">Settings</a>
</li>
<li class="af-side-nav-item">
<a href="#" class="af-side-nav-link">Account</a>
</li>
</ul>
</div>
</dialog>Right-side drawer
Section titled “Right-side drawer”Add af-drawer-right to open from the right edge:
<button class="af-btn"
onclick="document.getElementById('filters-drawer').showModal()">Open Filters</button>
<dialog class="af-drawer af-drawer-right" id="filters-drawer"
onclick="if (event.target === this) this.close()">
<div class="af-drawer-header">
<h3 class="af-drawer-title">Filters</h3>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-drawer-body">
<div class="af-stack af-gap-md">
<div class="af-field">
<label class="af-label" for="filter-status">Status</label>
<select id="filter-status" class="af-select">
<option>All</option>
<option>Active</option>
<option>Archived</option>
</select>
</div>
<div class="af-field">
<label class="af-label" for="filter-date-range">Date range</label>
<input id="filter-date-range" class="af-input" type="date">
</div>
<button class="af-btn" onclick="this.closest('dialog').close()">Apply Filters</button>
</div>
</div>
</dialog>Slide transition
Section titled “Slide transition”Add af-has-slide for a CSS-only slide (and backdrop fade). Uses @starting-style + transition-behavior: allow-discrete so native showModal() / close() can animate display — see Browser support below.
<button class="af-btn"
onclick="document.getElementById('slide-drawer').showModal()">Open with slide</button>
<dialog class="af-drawer af-has-slide" id="slide-drawer"
onclick="if (event.target === this) this.close()">
<div class="af-drawer-header">
<h3 class="af-drawer-title">Navigation</h3>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-drawer-body">
<p>This drawer slides in from the left.</p>
</div>
</dialog>Works with af-drawer-right the same way — the panel slides from the right edge.
--af-drawer-width defaults to 20rem. Override it globally or on one drawer. max-width: 90vw still applies so the panel never exceeds the viewport.
Global:
:root {
--af-drawer-width: 24rem;
}On one panel:
<dialog class="af-drawer" style="--af-drawer-width: 12rem">…</dialog>See Layout tokens for the full set (--af-sidebar-width vs --af-drawer-width).
Same as dialog. Open with showModal(), close with close().
const drawer = document.getElementById('my-drawer');
drawer.showModal();
drawer.close();| Class | Purpose |
|---|---|
af-drawer | The <dialog> element. Opens from the left by default. Width: --af-drawer-width. |
af-drawer-right | Modifier. Opens from the right instead. |
af-has-slide | Optional. Slide + backdrop fade on open/close. |
af-drawer-header | Header row with title and close button |
af-drawer-title | Title text inside the header |
af-drawer-body | Scrollable content area |
Use a quiet close control in the header: af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary.
Closing on backdrop click
Section titled “Closing on backdrop click”drawer.addEventListener('click', (e) => {
if (e.target === drawer) drawer.close();
});Accessibility
Section titled “Accessibility”Same accessibility model as dialog. Native <dialog> with showModal() marks the page as inert and provides ESC key and body scroll lock. Focus trapping and focus restoration require supplemental JS for full cross-browser reliability.
Motion respects prefers-reduced-motion via Airframe’s base styles (transition durations collapse).
Browser support
Section titled “Browser support”The drawer is a native <dialog> and opens everywhere. Only the optional af-has-slide transition depends on newer CSS:
| Feature | Chrome | Edge | Safari | Firefox |
|---|---|---|---|---|
@starting-style The opening keyframe to animate from | 117 Supported from 117 | 117 Supported from 117 | 17.5 Supported from 17.5 | 129 Supported from 129 |
transition-behavior: allow-discrete Transitioning display on close | 117 Supported from 117 | 117 Supported from 117 | 17.4 Supported from 17.4 | 129 Supported from 129 |
Safari versions cover iOS Safari too. Without support: the drawer opens and closes instantly instead of sliding. No JavaScript is involved either way. Versions checked against MDN browser-compat-data, August 2026. See Browser Support for Airframe's baseline.