Drawer
A panel that slides in from the edge of the screen. Supports snap-point resize, opt-in drag-to-dismiss, nested drawers, modal/non-modal mode, and four directional placement.
Anatomy
<Drawer.Provider bind:open direction="bottom">
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Handle />
<Drawer.Title>Drawer Title</Drawer.Title>
<Drawer.Description>Description text.</Drawer.Description>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Content>
</Drawer.Provider>
Parts
| Part |
Element |
Description |
Provider |
none |
Root context. Manages open state, direction, snap points. |
Trigger |
<button> |
Opens the drawer on click. |
Overlay |
<div> |
Backdrop behind the drawer. Click to dismiss. |
Content |
<div> |
Drawer panel. Resize drag, focus trap, scroll lock integrated. |
Handle |
<div> |
Visual drag handle. Drives resize when snap points are configured. |
Title |
<div> |
Drawer heading. Linked via aria-labelledby. |
Description |
<div> |
Drawer description. Linked via aria-describedby. |
Close |
<button> |
Unconditionally closes the drawer. |
ARIA
| Part |
Attribute |
Value |
| Trigger |
aria-haspopup |
dialog |
| Trigger |
aria-expanded |
true | false |
| Trigger |
aria-controls |
ID of Content |
| Content |
role |
dialog |
| Content |
aria-modal |
true (modal mode) |
| Content |
aria-labelledby |
ID of Title |
| Content |
aria-describedby |
ID of Description |
| Overlay |
aria-hidden |
true |
| Title |
role |
heading |
| Title |
aria-level |
Number (default 2) |
| Handle |
aria-hidden |
true |
Data Attributes
| Part |
Attribute |
Values |
| Trigger |
data-drawer-trigger |
Always present |
| Trigger |
data-state |
open | closed |
| Content |
data-drawer-content |
Always present |
| Content |
data-state |
open | closed |
| Content |
data-side |
top | right | bottom | left |
| Content |
data-dragging |
Present during active drag |
| Content |
data-nested |
Present when nested inside another drawer |
| Content |
data-nested-open |
Present when a nested child drawer is open |
| Overlay |
data-drawer-overlay |
Always present |
| Overlay |
data-state |
open | closed |
| Handle |
data-drawer-handle |
Always present |
| Title |
data-drawer-title |
Always present |
| Description |
data-drawer-description |
Always present |
| Close |
data-drawer-close |
Always present |
CSS Variables
| Variable |
Part |
Description |
--drawer-progress |
Content |
Drag progress 0–1 |
--drawer-offset-x |
Content |
Horizontal drag offset in px |
--drawer-offset-y |
Content |
Vertical drag offset in px |
--drawer-depth |
Content, Overlay |
Nesting depth (0 for first drawer) |
--drawer-overlay-opacity |
Overlay |
Computed opacity based on snap points |
Keyboard
| Key |
Action |
Escape |
Close the drawer (configurable via escapeKeydownBehavior) |
Tab |
Cycle focus within the drawer (modal mode, focus trapped) |
Usage
Basic
<script>
import { Drawer } from '$soma/components';
let open = $state(false);
</script>
<Drawer.Provider bind:open>
<Drawer.Trigger>Open Drawer</Drawer.Trigger>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Title>Settings</Drawer.Title>
<Drawer.Description>Configure your preferences.</Drawer.Description>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Content>
</Drawer.Provider>
Direction
<Drawer.Provider bind:open direction="right">
<!-- Slides in from the right edge -->
</Drawer.Provider>
Logical directions ('start', 'end') resolve RTL-aware: start → left in LTR, right in RTL.
Snap points
<Drawer.Provider bind:open snapPoints={[0.3, 0.6, 1]} bind:activeSnapPoint={snap}>
<!-- 30%, 60%, and fully open positions -->
</Drawer.Provider>
When snapPoints is omitted, drag works as free resize and release keeps the current
size. Enable dragToDismiss explicitly for the legacy no-snap swipe-to-close gesture.
<Drawer.Provider bind:open dragToDismiss>
<!-- No snap points: drag release can dismiss -->
</Drawer.Provider>
Non-modal
<Drawer.Provider bind:open variant="inline">
<!-- No overlay, no focus trap, no scroll lock -->
</Drawer.Provider>
Non-dismissible
<Drawer.Provider bind:open dismissible={false}>
<!-- Can only be closed via the open prop or Close button -->
</Drawer.Provider>
Nested drawers
<Drawer.Provider bind:open={outer}>
<Drawer.Content>
<Drawer.Provider bind:open={inner}>
<Drawer.Trigger>Open Nested</Drawer.Trigger>
<Drawer.Content>
<!-- data-nested, --drawer-depth: 1 -->
</Drawer.Content>
</Drawer.Provider>
</Drawer.Content>
</Drawer.Provider>