Drawer
A modal surface anchored to the bottom of the screen with a grab handle and drag-to-dismiss gesture.
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
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).
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.
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.
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.
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
titleand described bydescription. - 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.