Drawer
The <x-wirekit::drawer> component creates accessible side panels that slide in from any edge of the screen. Drawers share the same overlay behavior as modals (focus trap, scroll lock, events) but use slide transitions.
Usage
Width & Layout
Drawer width (for left/right) or height (for top/bottom) is controlled by the size prop:
| Size | Left/Right Width | Top/Bottom Height |
|---|---|---|
sm |
20rem (320px) | 30vh |
md |
24rem (384px) | 40vh |
lg |
32rem (512px) | 50vh |
xl |
40rem (640px) | 60vh |
full |
100% viewport | 100% viewport |
<x-wirekit::drawer name="filters" size="sm" position="right">…</x-wirekit::drawer>
Positions
<!-- Right (default) -->
<x-wirekit::drawer name="panel" position="right">
<!-- Left -->
<x-wirekit::drawer name="nav" position="left">
<!-- Top -->
<x-wirekit::drawer name="notification" position="top">
<!-- Bottom -->
<x-wirekit::drawer name="details" position="bottom">
Sizes
For left and right positions, size controls width. For top and bottom, size controls height.
| Size | Dimension | Value |
|---|---|---|
sm |
Width/Height | 20rem (320px) |
md |
Width/Height | 28rem (448px) |
lg |
Width/Height | 40rem (640px) |
<x-wirekit::drawer name="details" size="lg">
...
</x-wirekit::drawer>
With Livewire wire:model
<x-wirekit::drawer name="filters" wire:model="showFilters">
<x-wirekit::drawer.header>Filters</x-wirekit::drawer.header>
<x-wirekit::drawer.body>
<!-- Filter controls -->
</x-wirekit::drawer.body>
</x-wirekit::drawer>
When the user closes the drawer (ESC, backdrop, close button), the Livewire property is automatically set to false.
Keyboard
| Key | Action |
|---|---|
| Escape | Close drawer (when dismissible) |
| Tab / Shift+Tab | Cycle focus within drawer (focus trap) |
Livewire Integration
wire:modelsyncs drawer open/close state with a Livewire propertywire:ignore.selfprevents Livewire DOM morphing issueswire:navigate(SPA mode) automatically closes open drawers
Behavior
- Focus trap (bundled ~3.8 KB) prevents keyboard navigation outside the drawer
- Scroll lock hides body scrollbar while drawer is open
- Slide transition direction matches the drawer position
- Teleport renders drawer at
<body>level for correct stacking - Shared overlay logic with Modal — identical focus trap, scroll lock, and event handling
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
required | Unique drawer identifier |
position |
string |
'right' |
Slide direction: left, right, top, bottom |
size |
string |
'md' |
Width (left/right) or height (top/bottom) |
dismissible |
bool |
true |
Whether ESC and backdrop click close the drawer |
describedby |
string|null |
null |
Element ID referenced by the dialog's aria-describedby (wire to <x-wirekit::drawer.body id="...">) |
label |
string|null |
null |
An explicit accessible name, for a drawer composed without <x-wirekit::drawer.header>. Without it such a drawer announces as a bare "dialog". It is a prop rather than something the drawer detects, because the header binds its id through Alpine and is not visible to the parent at render time — which also means the name survives with JavaScript disabled. WCAG 2.1 4.1.2 (Level A). |
scope |
string|null |
null |
Scoped personalization key |
Sub-Components
| Component | Purpose |
|---|---|
drawer.header |
Title area at the top with bottom border + auto close-X (dismissible only) |
drawer.body |
Scrollable content area (uses flex-1) |
drawer.footer |
Action buttons area with top border |
drawer.close |
Wraps any element to make it close the drawer |
Auto Close Button
Like modal.header, drawer.header renders a close-X in the top-right corner by default, gated at runtime with x-show="dismissible" so non-dismissible drawers stay clean. Opt out with :close="false" when you want to control the layout yourself:
<x-wirekit::drawer.header :close="false">
<div class="flex items-center gap-2">
<x-wirekit::icon name="settings" class="h-5 w-5" />
Settings
</div>
</x-wirekit::drawer.header>
Personalize the button's classes via WireKit::personalize('drawer.header', ['close' => 'your-classes']).
Unlike Modal, Drawer has no drawer.trigger sub-component. Drawers are typically triggered from navbars or other locations far from the drawer itself. Use events or wire:model to open drawers programmatically.
Events
Opening
<!-- Alpine -->
<button x-on:click="$dispatch('wirekit-drawer-show', { name: 'settings' })">
<!-- Livewire (server-side) -->
$this->dispatch('wirekit-drawer-show', name: 'settings');
<!-- Vanilla JS -->
window.dispatchEvent(new CustomEvent('wirekit-drawer-show', { detail: { name: 'settings' } }));
Closing
<!-- Alpine -->
<button x-on:click="$dispatch('wirekit-drawer-close', { name: 'settings' })">
<!-- Livewire (server-side) -->
$this->dispatch('wirekit-drawer-close', name: 'settings');
Accessibility
- Dialog:
role="dialog",aria-modal="true",aria-labelledby - Focus trap active — Tab cycles within the drawer only
- Focus returns to the previously focused element after close
- Close button:
aria-label="Close" - Backdrop:
aria-hidden="true"(decorative)
Keyboard Interaction
| Key | Action |
|---|---|
Tab / Shift+Tab |
Cycle focus inside the drawer (focus is trapped) |
Escape |
Close the drawer (when closeOnEsc is not disabled) |
| Click overlay | Close the drawer (when closeOnOverlay is not disabled) |
Pitfalls
- Don't put the trigger button (the one that dispatches
wirekit:drawer:open) outside its parent<x-wirekit::drawer>. The trigger reads its target via the named slot context — orphaned triggers do nothing. - Don't bind
wire:clickon the trigger. The Alpine open handler dispatches the open event; a competingwire:clickwill run before the open and may navigate away. - Don't disable
closeOnEscfor non-modal flows. Drawers without an Escape exit are a WCAG 2.1.2 (No Keyboard Trap) trap — keep the default unless you have a hard requirement.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Drawer font family |
--font-wk-heading-weight |
Header font weight |
--text-wk-md / --text-wk-lg |
Body / heading font size |
--color-wk-text |
Body text |
--color-wk-text-muted |
Header subtitle text |
--color-wk-bg-elevated |
Drawer panel background |
--color-wk-bg-muted |
Footer background |
--color-wk-border / --color-wk-border-subtle |
Panel + footer divider border |
--color-wk-overlay |
Backdrop scrim color |
--color-wk-ring |
Close-button focus ring |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-sm |
Close-button border radius |
--shadow-wk-lg |
Panel drop shadow |
--size-wk-drawer-sm / --size-wk-drawer-md / --size-wk-drawer-lg |
Panel width per size prop |
--size-wk-drawer-inset |
The strip of backdrop left showing beside an open drawer. Every size above is wider than a phone, so the panel is clamped against the viewport; this is how much of the page stays visible, and it is what keeps the drawer reading as a sheet over the page rather than as a second page. Lower it for a dense console, raise it for more page |
--size-wk-sm |
Close-button hit target |
--gap-wk-md |
Footer-button gap |
--padding-wk-x-sm / --padding-wk-x-lg / --padding-wk-x-xl |
Horizontal padding (close button + panel sections) |
--padding-wk-y-md / --padding-wk-y-xl |
Vertical padding |
--z-wk-drawer |
Stacking context |
Further Reading
- WAI-ARIA Dialog Pattern — drawers are a dialog variant
- MDN:
role="dialog" - MDN:
aria-modal - focus-trap — focus management library (bundled, ~3.8 KB)