1 Drawer
L4stIdi0t edited this page 2026-09-09 00:02:12 +02:00

Drawer

A bottom sheet that settles at more than one height — the pattern a phone map uses: a peek showing the headline, a half height for the list, and a full height for the detail, moved between by dragging the grabber or by the keyboard.

snapPoints are fractions of the viewport height, smallest first, and snap is the index it is resting at — bindable, so the app can move it (a search result arrives, so open to half) and can watch the reader move it back. The drag is followed frame by frame and released with its momentum: where it lands is where the gesture was heading, not where the finger happened to stop. Past the smallest point a dismissible drawer closes, which is the gesture everyone already knows. Under prefers-reduced-motion it jumps between heights instead of gliding. The grabber is a real control, not a decoration: an APG window splitter, so ↑/↓ move between the snap points, Home/End reach the extremes, and a screen reader is told which of them it is on. Dragging is never the only way to resize. Modal by default — backdrop, focus trap, the page inert, body scroll locked. modal={false} is the map case: the page behind stays live while the drawer peeks over it.

Overlays · two-way: bind:open bind:snap · portalled to <body> · keyboard contract

<script lang="ts">
	import { Drawer } from '@l4st/driftwood';
</script>
Light Dark
drawer drawer in dark mode

Props

Prop Type Default What it does
open bind: boolean false Opening slides it up to snap.
snapPoints number[] [0.35, 0.9] The heights it settles at, as fractions of the viewport, smallest first.
snap bind: number 0 Index of the snap point it is resting at. Bindable both ways.
dismissible boolean true A drag below the smallest snap point closes it.
modal boolean true Traps focus, inerts the page and dims it. false leaves the page live under a peek.
title string — Names the drawer for assistive tech (aria-labelledby).
description string — One line under the title; also its aria-describedby.
class string — Merged with the component's own classes, last — so yours win.
handleLabel string 'Drawer height' Accessible name of the grabber, which is the resize control.
closeLabel string 'Close' Accessible name of the close button. Hidden entirely when dismissible is false.
onSnapChange (snap: number) => void — Fires with the index it settles at, however it got there.
children required Snippet — The content.
footer Snippet — Actions along the bottom, above the safe area. Put the confirming button last.

Keyboard

Key Does
↑ / ↓ move between snap points, from the grabber — ↓ at the lowest one closes
Home / End the smallest / tallest snap point
Tab / Shift+Tab cycle within a modal drawer — focus is trapped
Escape close and return focus to the opener

Notes

Notes

  • Snap points are fractions of the viewport, smallest first. [0.35, 0.9] is a peek and a nearly-full height. Three is usually the most that is worth it: past that, nobody can tell which one they are on.
  • bind:snap runs both ways. The app can move the drawer — a result arrives, so open it to the middle — and can watch the reader move it back. onSnapChange is the same information as a callback, for the cases where a binding is awkward.
  • modal={false} is the map case. The default drawer dims the page, traps focus and locks the page's scroll, which is what a drawer over a form wants. A drawer over something the reader is still using — a map, a canvas, a video — should not do any of that. A non-modal drawer still closes on Escape and still moves focus into itself when it opens; it just does not hold on to it.
  • The grabber is a control, not a decoration. It is an APG window splitter: Tab reaches it, ↑ and ↓ move between the snap points, Home and End reach the ends, and ↓ at the lowest one closes a dismissible drawer. A pointer drag is an enhancement over that, never the only way — dragging is a gesture plenty of people cannot make.
  • The drag is released with its momentum. Where it lands is where the gesture was heading a tenth of a second later, so a flick carries past the nearest stop and a slow drag does not. Under prefers-reduced-motion it jumps between heights with no glide at all.
  • The backdrop fades with the height. A peek dims the page less than a full height does, so the drag has a visible consequence beyond the panel itself.
  • dismissible={false} means it stays. No close button, no Escape, and a drag below the smallest snap point springs back — for the drawer that is the screen rather than something over it.

Overlays · Dialog · Sheet · Tooltip · Popover


← All components · Getting started · Theming · source