Skip to main content
WireKit
Copy for LLM

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

Drawer Panel

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:model syncs drawer open/close state with a Livewire property
  • wire:ignore.self prevents Livewire DOM morphing issues
  • wire: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:click on the trigger. The Alpine open handler dispatches the open event; a competing wire:click will run before the open and may navigate away.
  • Don't disable closeOnEsc for 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

Was this page helpful?

Thanks — that helps.

Voting requires cookies or local storage. What we store