---
title: Drawer
description: Slide-out panel from screen edge
visibility: guest
draft: false
---

# 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

:::preview{title="Drawer Panel"}
<div x-data>
<x-wirekit::drawer name="settings-drawer">
    <x-wirekit::drawer.header>Settings</x-wirekit::drawer.header>
    <x-wirekit::drawer.body>Settings form content goes here...</x-wirekit::drawer.body>
    <x-wirekit::drawer.footer>
        <x-wirekit::drawer.close>
            <x-wirekit::button intent="neutral" surface="ghost" size="sm">Cancel</x-wirekit::button>
        </x-wirekit::drawer.close>
        <x-wirekit::button size="sm">Save</x-wirekit::button>
    </x-wirekit::drawer.footer>
</x-wirekit::drawer>
<x-wirekit::button @click="$dispatch('wirekit-drawer-show', { name: 'settings-drawer' })">Open Drawer</x-wirekit::button>
</div>
:::

:::source{language="blade"}
<x-wirekit::drawer name="settings-drawer">
    <x-wirekit::drawer.header>Settings</x-wirekit::drawer.header>
    <x-wirekit::drawer.body>Settings form content goes here...</x-wirekit::drawer.body>
    <x-wirekit::drawer.footer>
        <x-wirekit::drawer.close>
            <x-wirekit::button intent="neutral" surface="ghost" size="sm">Cancel</x-wirekit::button>
        </x-wirekit::drawer.close>
        <x-wirekit::button size="sm" wire:click="save">Save</x-wirekit::button>
    </x-wirekit::drawer.footer>
</x-wirekit::drawer>

<x-wirekit::button @click="$dispatch('wirekit-drawer-show', { name: 'settings-drawer' })">
    Open Drawer
</x-wirekit::button>
:::

## 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 |

```blade
<x-wirekit::drawer name="filters" size="sm" position="right">…</x-wirekit::drawer>
```

## Positions

```blade
<!-- 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) |

```blade
<x-wirekit::drawer name="details" size="lg">
    ...
</x-wirekit::drawer>
```

## With Livewire `wire:model`

```blade
<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](https://github.com/focus-trap/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:

```blade
<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'])`.

::: info
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

```blade
<!-- 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

```blade
<!-- 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

- [WAI-ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) — drawers are a dialog variant
- [MDN: `role="dialog"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role)
- [MDN: `aria-modal`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal)
- [focus-trap](https://github.com/focus-trap/focus-trap) — focus management library (bundled, ~3.8 KB)
