# Drawer

A modal surface anchored to the bottom of the screen with a grab handle and drag-to-dismiss gesture.

```slint
import { Button } from "@glint/components/button.slint";
import { Drawer } from "@glint/components/drawer.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 380px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Open drawer";
                haspopup: true;
                clicked => { drawer.open = true; }
            }
        }
    }

    drawer := Drawer {
        width: parent.width;
        height: parent.height;
        title: "Activity log";
        description: "Recent updates and notifications from your team.";

        VerticalLayout {
            spacing: 8px;

            Text {
                text: "• Deployment #148 completed successfully";
                color: Tokens.color-foreground;
                font-size: Tokens.typography-body-sm-size;
            }

            Text {
                text: "• Ana Silva merged pull request #42";
                color: Tokens.color-foreground;
                font-size: Tokens.typography-body-sm-size;
            }
        }
    }
}
```

## Usage

```slint
import { Drawer } from "@glint/components/drawer.slint";

export component AppWindow inherits Window {
    in-out property <bool> show-drawer: false;
    callback log-closed();

    VerticalLayout {
        // your screen layout
    }

    // Last child of the window, sized to it: Slint has no portals, so what
    // draws on top is what comes last.
    Drawer {
        width: parent.width;
        height: parent.height;
        panel-extent: 320px;
        open <=> root.show-drawer;
        title: "Filters";
        description: "Configure view options.";
        dismissed => { root.log-closed(); }
    }
}
```

`Drawer` is a bottom-anchored modal surface designed for gesture-friendly interactions. It features a drag handle at the top and supports drag-to-dismiss gestures alongside standard backdrop and keyboard dismissal.

Mount it **last** among the window’s children, sized to `width: parent.width; height: parent.height`.

## Examples

### Snap points

`snap-points` accepts an array of fractions of `panel-extent` (in ascending order) where the drawer can rest. Dragging between snap points snaps to the closest threshold and reports the new index through `current-snap` and `snap-changed(int)`.

```slint
import { Button } from "@glint/components/button.slint";
import { Drawer } from "@glint/components/drawer.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 380px;
    background: Tokens.color-background;

    in-out property <int> active-snap: 0;

    VerticalLayout {
        alignment: center;
        spacing: 12px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Open with snap points";
                haspopup: true;
                clicked => { drawer.open = true; }
            }
        }

        Text {
            text: "Resting height snap: " + (root.active-snap == 0 ? "Peek (40%)" : "Full (100%)");
            color: Tokens.color-muted-foreground;
            horizontal-alignment: center;
        }
    }

    drawer := Drawer {
        width: parent.width;
        height: parent.height;
        panel-extent: 320px;
        snap-points: [0.4, 1.0];
        current-snap <=> root.active-snap;
        title: "Media playlist";
        description: "Drag the handle to expand or collapse between snap points.";
    }
}
```

### Non-modal drawer

Setting `modal: false` removes the backdrop scrim entirely, allowing users to interact with the underlying interface while the drawer remains open.

```slint
import { Button } from "@glint/components/button.slint";
import { Drawer } from "@glint/components/drawer.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 380px;
    background: Tokens.color-background;

    in-out property <int> counter: 0;

    VerticalLayout {
        alignment: center;
        spacing: 12px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;
            spacing: 8px;

            Button {
                text: "Toggle drawer";
                haspopup: true;
                clicked => { drawer.open = !drawer.open; }
            }

            Button {
                text: "Background action (" + root.counter + ")";
                clicked => { root.counter += 1; }
            }
        }
    }

    drawer := Drawer {
        width: parent.width;
        height: parent.height;
        modal: false;
        panel-extent: 180px;
        title: "Docked summary";
        description: "The background controls remain interactive.";
    }
}
```

### Nested drawers

Stacking multiple drawers is supported through `nested-open`. When a child drawer opens, the parent drawer dims its own backdrop and slides down slightly (`nested-inset`) so the pair reads as a depth stack.

```slint
import { Button } from "@glint/components/button.slint";
import { Drawer } from "@glint/components/drawer.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 380px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            Button {
                text: "Open parent drawer";
                haspopup: true;
                clicked => { parent-drawer.open = true; }
            }
        }
    }

    parent-drawer := Drawer {
        width: parent.width;
        height: parent.height;
        panel-extent: 320px;
        title: "Settings";
        description: "Primary configuration options.";
        nested-open: child-drawer.open;

        HorizontalLayout {
            alignment: center;
            padding-top: 12px;

            Button {
                text: "Advanced options";
                haspopup: true;
                clicked => { child-drawer.open = true; }
            }
        }
    }

    child-drawer := Drawer {
        width: parent.width;
        height: parent.height;
        panel-extent: 240px;
        title: "Advanced options";
        description: "Fine-tune system parameters.";
    }
}
```

### Restoring focus

`restore-focus` fires whenever the drawer closes, allowing keyboard focus to return cleanly to the opening button.

```slint
import { Button } from "@glint/components/button.slint";
import { Drawer } from "@glint/components/drawer.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 380px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            trigger := Button {
                text: "Open notifications";
                haspopup: true;
                clicked => { drawer.open = true; }
            }
        }
    }

    drawer := Drawer {
        width: parent.width;
        height: parent.height;
        title: "Notifications";
        description: "Focus returns to the trigger button when dismissed.";
        restore-focus => { trigger.focus(); }
    }
}
```

## API Reference

### Properties

| Property              | Type          | Default    | Description                                                                                                                                                                                                                                                                      |
| --------------------- | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open`                | `in-out bool` | `false`    | Two-way; consumer sets true to slide up, Drawer sets false on close.                                                                                                                                                                                                             |
| `title`               | `in string`   | no default | Heading inside the drawer.                                                                                                                                                                                                                                                       |
| `description`         | `in string`   | no default | Body text under the title.                                                                                                                                                                                                                                                       |
| `dismiss-on-backdrop` | `in bool`     | `true`     | When true, clicking the backdrop closes the drawer.                                                                                                                                                                                                                              |
| `modal`               | `in bool`     | `true`     | When false the drawer renders no backdrop at all, so the application behind it keeps its pointer events (shadcn's non-modal drawer). An open `Scrim` installs a full-window TouchArea, so suppressing it is the only way the content underneath stays live.                      |
| `panel-extent`        | `in length`   | `360px`    | Height of the panel; the drawer always spans the window's width.                                                                                                                                                                                                                 |
| `snap-points`         | `in [float]`  | `[]`       | Heights the drawer can come to rest at, as fractions of `panel-extent`, in ascending order. Empty (the default) means one resting height — the full extent — and the drag stays the two-outcome gesture it always was.                                                           |
| `current-snap`        | `in-out int`  | `0`        | Index into `snap-points` of the height the drawer is resting at. Two-way: the consumer picks the opening height, and a settled drag writes back the one it landed on.                                                                                                            |
| `nested-open`         | `in bool`     | `false`    | True while another Drawer is open on top of this one. Set it from the child's `open` — one line, the way ADR-0006 wires every trigger. The stack then shows a single backdrop (this one stands its own down) and this panel insets behind the child instead of doubling the dim. |
| `nested`              | `out bool`    | no default | True while this drawer is open with something stacked on it — the styling hook vaul spells `data-nested-drawer-open`.                                                                                                                                                            |
| `drag-offset`         | `out length`  | no default | How far the panel is currently dragged below its resting position; zero unless a drag is in flight. Public because the gesture's progress is the one piece of drawer state a consumer cannot derive.                                                                             |

### Callbacks

| Callback            | Description                                                                                                                                                                                                                                                     |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dismissed()`       | Fired when the drawer closes itself — backdrop, Escape or drag.                                                                                                                                                                                                 |
| `snap-changed(int)` | Fired when a settled drag comes to rest at a different snap point, with its index. `current-snap` already carries the new value.                                                                                                                                |
| `restore-focus()`   | Fired on EVERY close, including one the consumer drives by setting `open = false`, so keyboard focus always gets a new home: `restore-focus => { my-button.focus(); }`. Slint exposes no "previously focused element", so only the trigger's owner can name it. |

## Accessibility

- **Role and name.** The open drawer card claims a region landmark named by `title` and described by `description`.
- **Grab handle.** A pill-shaped grab indicator at the top of the panel provides a clear visual affordance for the drag gesture, backed by a generous touch grab target.
- **Keyboard navigation.** `Escape` dismisses the open drawer and calls `dismissed()`.
- **Focus restoration.** Every close path — backdrop click, drag release past the dismiss threshold, Escape, or programmatic close — invokes `restore-focus()` to return focus to the trigger.
- **Non-modal accessibility.** With `modal: false`, the scrim TouchArea is removed so screen readers and keyboard users can continue accessing the application content behind the drawer.
- **The trigger.** Controls that open a drawer should declare `haspopup: true`.
