# Drawer

> A panel that comes in from the edge — the same <dialog> as a modal, in a different shape.

A drawer is a full-height panel anchored to one side. Like `modal()`, it is a `<dialog>`: a button with a matching `commandfor` and `command: 'show-modal'` opens it, and the browser handles the backdrop, focus, the outside click and Escape.

Its most common job on a static site is the navigation menu on a phone.

## Basic drawer

```javascript
fragment(
  button({ commandfor: 'drawer-basic', command: 'show-modal' }, 'Open drawer'),
  drawer({ id: 'drawer-basic', title: 'Settings' },
    stack({ gap: 'md' },
      toggle({ label: 'Pagefind search', checked: true }),
      toggle({ label: 'Image optimization', checked: true }),
      toggle({ label: 'Server islands' }),
    ),
  ),
)
```

## Sides

```javascript
fragment(
  stack({ direction: 'row', gap: 'sm' },
    button({ variant: 'outline', color: 'neutral', commandfor: 'drawer-start', command: 'show-modal' }, 'From the start'),
    button({ variant: 'outline', color: 'neutral', commandfor: 'drawer-end', command: 'show-modal' }, 'From the end'),
  ),
  drawer({ id: 'drawer-start', side: 'start', title: 'Start' },
    text({ variant: 'small', tone: 'muted' }, 'Anchored to the leading edge — the left in a left-to-right language.'),
  ),
  drawer({ id: 'drawer-end', title: 'End' },
    text({ variant: 'small', tone: 'muted' }, 'The default: anchored to the trailing edge.'),
  ),
)
```

## Width

Any CSS length. It is capped at 90% of the viewport, so a wide drawer still fits a phone.

```javascript
fragment(
  stack({ direction: 'row', gap: 'sm' },
    button({ variant: 'outline', color: 'neutral', commandfor: 'drawer-narrow', command: 'show-modal' }, 'Narrow'),
    button({ variant: 'outline', color: 'neutral', commandfor: 'drawer-wide', command: 'show-modal' }, 'Wide'),
  ),
  drawer({ id: 'drawer-narrow', width: '14rem', title: 'Narrow' },
    text({ variant: 'small', tone: 'muted' }, 'width: 14rem'),
  ),
  drawer({ id: 'drawer-wide', width: '34rem', title: 'Wide' },
    text({ variant: 'small', tone: 'muted' }, 'width: 34rem'),
  ),
)
```

## As a navigation menu

The pattern most sites want: a menu button in the bar, the links in a drawer.

```javascript
fragment(
  appBar({ brand: 'sitelo' },
    appBarSpacer(),
    appBarActions(
      iconButton({
        label: 'Open navigation',
        variant: 'ghost',
        color: 'neutral',
        commandfor: 'drawer-nav', command: 'show-modal',
        icon: icon('menu'),
      }),
    ),
  ),
  drawer({ id: 'drawer-nav', title: 'Navigation' },
    navLink({ href: '#docs', current: true }, 'Docs'),
    navLink({ href: '#ui' }, 'UI'),
    navLink({ href: '#examples' }, 'Examples'),
    navLink({ href: '#about' }, 'About'),
    divider({ spacing: 'sm' }),
    button({ block: true }, 'Get started'),
  ),
)
```

## A filter panel

```javascript
fragment(
  button({ variant: 'soft', color: 'neutral', commandfor: 'drawer-filters', command: 'show-modal' }, 'Filters'),
  drawer({ id: 'drawer-filters', title: 'Filters', width: '22rem' },
    stack({ gap: 'lg' },
      choiceGroup({
        legend: 'Type',
        name: 'drawer-type',
        value: 'guide',
        options: [
          { value: 'guide', label: 'Guides' },
          { value: 'example', label: 'Examples' },
          { value: 'all', label: 'Everything' },
        ],
      }),
      choiceGroup({
        legend: 'Tags',
        name: 'drawer-tags',
        type: 'checkbox',
        value: ['routing'],
        options: ['routing', 'data', 'islands'],
      }),
      stack({ direction: 'row', gap: 'sm' },
        button({ variant: 'ghost', color: 'neutral', commandfor: 'drawer-filters', command: 'close' }, 'Cancel'),
        button('Apply'),
      ),
    ),
  ),
)
```

## Background scrolling

The page behind an open drawer does not scroll — the same CSS-only lock `modal()` uses, with no script and nothing to initialise. Pass `lockScroll: false` to let the background scroll as usual.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Required. What a trigger’s commandfor points at. |
| `title` | `Child` | — | Heading, and the dialog’s accessible name. |
| `side` | `'start' \| 'end'` | `'end'` | Which edge it is anchored to. |
| `width` | `string` | `'20rem'` | Panel width, capped at 90vw. |
| `closable` | `boolean` | `true` | Show the × in the header. |
| `closeLabel` | `string` | `'Close'` | Accessible name for that button. |
| `lockScroll` | `boolean` | `true` | Stop the page behind it scrolling while it is open. |
