Drawer
Accessible, unstyled side drawer (sheet) built on native <dialog> and the Dialog contract.
|
|
| Package |
@vizejs/ui/drawer |
| Maturity |
stable |
| Own the source |
vize lib pull drawer |
| Requires |
context, controllable-state, dialog, dismissable-layer, focus-scope, id, scroll-lock |
| Aliases |
sheet, bottom sheet, side sheet, off-canvas, vaul |
| Covers |
HTML dialog element, WAI-ARIA dialog pattern, Vaul Drawer, Radix Dialog sheet recipes |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Drawer, DrawerRoot, DrawerContent, DrawerHandle } from "@vizejs/ui/drawer";
Or copy the source into your project with vize lib pull drawer (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Side drawer with a drag handle that holds secondary filter controls. -->
<script setup lang="ts">
import { ref } from "vue";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerTitle,
DrawerTrigger,
} from "@vizejs/ui/drawer";
const open = ref(false);
</script>
<template>
<Drawer v-model:open="open" side="right">
<DrawerTrigger>Open filters</DrawerTrigger>
<DrawerContent>
<DrawerHandle />
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription>Narrow the order list by status.</DrawerDescription>
<fieldset>
<legend>Status</legend>
<label><input type="checkbox" name="status" value="paid" /> Paid</label>
<label><input type="checkbox" name="status" value="refunded" /> Refunded</label>
</fieldset>
<DrawerClose>Apply filters</DrawerClose>
</DrawerContent>
</Drawer>
</template>
API
Drawer
Source: src/families/overlays/drawer/drawer-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned Drawer base id. null and undefined select a deterministic fallback. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
modal |
boolean |
true |
Show the native <dialog> with showModal() (top layer, inert page, ::backdrop). |
side |
DrawerSide |
"bottom" |
Viewport edge the drawer is attached to and dragged toward to dismiss. |
snapPoints |
readonly DrawerSnapPoint[] |
undefined |
Resting positions: fractions in (0, 1] of the drawer size or "<number>px" visible sizes. undefined means a single fully open position. |
activeSnapPoint |
DrawerSnapPoint | null |
undefined |
Controlled active snap point. undefined selects uncontrolled behavior. |
defaultActiveSnapPoint |
DrawerSnapPoint |
undefined |
Initial snap point for uncontrolled use. undefined selects the first snap point. |
dismissible |
boolean |
true |
Whether Escape, backdrop presses, outside presses, and drag gestures may dismiss. |
closeThreshold |
number |
0.25 |
Fraction of the size still visible at the lowest snap point that a drag must pass to dismiss. |
velocityThreshold |
number |
0.5 |
Release velocity in px/ms toward the dismiss direction that flicks the drawer closed. |
Events
| Event |
Payload |
Description |
update:open |
[value: boolean] |
Fired when the Drawer requests a controlled open value. |
open-change |
[value: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request. |
update:activeSnapPoint |
[value: DrawerSnapPoint | null] |
Fired when the Drawer requests a controlled snap point. |
snap-point-change |
[ value: DrawerSnapPoint | null, previous: DrawerSnapPoint | null, nativeEvent: Event | null, ] |
Fired after any distinct snap point request. |
drag-start |
[nativeEvent: PointerEvent] |
Fired once a pointer has moved past the drag slop. |
drag-end |
[event: DrawerDragEndEvent] |
Fired when a started drag gesture ends. |
Slots
| Slot |
Slot props |
Description |
default |
DrawerSlotState |
Compound Drawer children. Receives the current open, snap, and drag state. |
Exposed
| Member |
Type |
Description |
activeSnapPoint |
— |
|
close |
— |
|
contentId |
— |
|
descriptionId |
— |
|
dragging |
— |
|
id |
— |
|
modal |
— |
|
open |
— |
|
openDrawer |
— |
|
setOpen |
— |
|
side |
— |
|
snapPoints |
— |
|
snapTo |
— |
|
state |
— |
|
titleId |
— |
|
toggle |
— |
|
DrawerRoot
Source: src/families/overlays/drawer/drawer-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned Drawer base id. null and undefined select a deterministic fallback. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
modal |
boolean |
true |
Show the native <dialog> with showModal() (top layer, inert page, ::backdrop). |
side |
DrawerSide |
"bottom" |
Viewport edge the drawer is attached to and dragged toward to dismiss. |
snapPoints |
readonly DrawerSnapPoint[] |
undefined |
Resting positions: fractions in (0, 1] of the drawer size or "<number>px" visible sizes. undefined means a single fully open position. |
activeSnapPoint |
DrawerSnapPoint | null |
undefined |
Controlled active snap point. undefined selects uncontrolled behavior. |
defaultActiveSnapPoint |
DrawerSnapPoint |
undefined |
Initial snap point for uncontrolled use. undefined selects the first snap point. |
dismissible |
boolean |
true |
Whether Escape, backdrop presses, outside presses, and drag gestures may dismiss. |
closeThreshold |
number |
0.25 |
Fraction of the size still visible at the lowest snap point that a drag must pass to dismiss. |
velocityThreshold |
number |
0.5 |
Release velocity in px/ms toward the dismiss direction that flicks the drawer closed. |
Events
| Event |
Payload |
Description |
update:open |
[value: boolean] |
Fired when the Drawer requests a controlled open value. |
open-change |
[value: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request. |
update:activeSnapPoint |
[value: DrawerSnapPoint | null] |
Fired when the Drawer requests a controlled snap point. |
snap-point-change |
[ value: DrawerSnapPoint | null, previous: DrawerSnapPoint | null, nativeEvent: Event | null, ] |
Fired after any distinct snap point request. |
drag-start |
[nativeEvent: PointerEvent] |
Fired once a pointer has moved past the drag slop. |
drag-end |
[event: DrawerDragEndEvent] |
Fired when a started drag gesture ends. |
Slots
| Slot |
Slot props |
Description |
default |
DrawerSlotState |
Compound Drawer children. Receives the current open, snap, and drag state. |
Exposed
| Member |
Type |
Description |
open |
boolean |
Whether the drawer is open. |
modal |
boolean |
Whether the drawer is shown as a modal <dialog>. |
state |
DrawerState |
Stable state token for styling and tests. |
side |
DrawerSide |
Viewport edge the drawer is attached to. |
activeSnapPoint |
DrawerSnapPoint | null |
Snap point the drawer currently rests on, or null without snap points. |
dragging |
boolean |
Whether a drag gesture is in progress. |
id |
string |
Root-owned base id. |
contentId |
string |
Id of the rendered <dialog>. |
titleId |
string |
Default id consumed by DrawerTitle. |
descriptionId |
string |
Default id consumed by DrawerDescription. |
snapPoints |
readonly DrawerSnapPoint[] |
Configured snap points in consumer order. |
setOpen |
(value: boolean, event?: Event | null) => boolean |
Request a specific open value and report whether it differs. |
openDrawer |
(event?: Event | null) => boolean |
Request the open state. |
close |
(event?: Event | null) => boolean |
Request the closed state. |
toggle |
(event?: Event | null) => boolean |
Request the opposite open state. |
snapTo |
(snapPoint: DrawerSnapPoint, event?: Event | null) => boolean |
Request a snap point and report whether it differs. Unknown values are ignored. |
DrawerContent
Source: src/families/overlays/drawer/drawer-content.vue
Props
| Prop |
Type |
Default |
Description |
forceMount |
boolean |
false |
Render slot contents while the drawer is closed. |
autoFocus |
boolean |
true |
Move focus into the drawer when it opens. |
restoreFocus |
boolean |
true |
Restore focus to the trigger (or previous focus) when the drawer closes. |
lockScroll |
boolean |
true |
Lock document scroll while a modal drawer is open. |
closeOnEscape |
boolean |
true |
Let Escape (and the native cancel event) request dismissal. |
closeOnBackdropPointerDown |
boolean |
true |
Let a press on the modal ::backdrop request dismissal. |
closeOnPointerDownOutside |
boolean |
true |
Let a press outside a non-modal drawer request dismissal. |
dragFromContent |
boolean |
true |
Start drag gestures anywhere on the content, not only on DrawerHandle. |
ariaLabel |
string |
undefined |
Accessible name when no visible title supplies one. |
ariaLabelledby |
string | null |
undefined |
Space-separated ids that label the drawer. null omits the default title id. |
ariaDescribedby |
string | null |
undefined |
Space-separated ids that describe the drawer. null omits the default description id. |
Events
| Event |
Payload |
Description |
open-auto-focus |
[event: DrawerAutoFocusEvent] |
Fired before automatic entry focus is applied. |
close-auto-focus |
[event: DrawerAutoFocusEvent] |
Fired before automatic focus restoration is applied. |
escape-key-down |
[event: DrawerEscapeKeyDownEvent] |
Fired before Escape requests dismissal. |
pointer-down-outside |
[event: DrawerPointerDownOutsideEvent] |
Fired before an outside pointer-down requests dismissal of a non-modal drawer. |
backdrop-pointer-down |
[event: DrawerBackdropPointerDownEvent] |
Fired before a modal ::backdrop press requests dismissal. |
dismiss |
[event: DrawerDismissEvent] |
Fired after an unprevented dismissal request. |
Slots
| Slot |
Slot props |
Description |
default |
DrawerSlotState |
Drawer content. Receives the current open, snap, and drag state. |
Exposed
| Member |
Type |
Description |
open |
boolean |
Whether the drawer is open. |
modal |
boolean |
Whether the drawer is shown as a modal <dialog>. |
state |
DrawerState |
Stable state token for styling and tests. |
side |
DrawerSide |
Viewport edge the drawer is attached to. |
activeSnapPoint |
DrawerSnapPoint | null |
Snap point the drawer currently rests on, or null without snap points. |
dragging |
boolean |
Whether a drag gesture is in progress. |
element |
HTMLDialogElement | null |
Rendered native dialog element. |
snapOffset |
number |
Resolved snap offset in px toward the dismiss direction. |
dragOffset |
number |
Current drag offset in px toward the dismiss direction. |
focusContent |
(options?: FocusOptions) => void |
Move focus to the dialog element. |
DrawerHandle
Source: src/families/overlays/drawer/drawer-handle.vue
Props
| Prop |
Type |
Default |
Description |
disabled |
boolean |
false |
Remove the handle from activation and sequential keyboard focus. Dragging from content still works. |
ariaLabel |
string |
"Resize drawer" |
Accessible name of the handle button. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the handle steps to the next snap point. Call preventDefault() to keep it. |
Slots
| Slot |
Slot props |
Description |
default |
DrawerSlotState |
Optional grip contents. Receives the current Drawer state. |
Exposed
| Member |
Type |
Description |
element |
HTMLButtonElement | null |
Rendered native button. |
focus |
(options?: FocusOptions) => void |
Move focus to the handle. |
Behavior
Normative state x input -> outcome table for @vizejs/ui/drawer (drawer-root.vue,
drawer-content.vue, drawer-handle.vue). The trigger, title, description, and close parts are
the Dialog SFCs (dialog-trigger.vue, dialog-title.vue, dialog-description.vue,
dialog-close.vue) re-exported as DrawerTrigger, DrawerTitle, DrawerDescription, and
DrawerClose; DrawerRoot provides the shared Dialog context they consume. Every row names the
test that proves it (drawer.test.ts, drawer-ssr.test.ts, or drawer.types.test-d.ts).
| Surface |
Contract |
| Native |
DrawerContent renders a native <dialog>: showModal() when open and modal (top layer, inert page, ::backdrop), show() when non-modal, close() when closed. |
| State |
DrawerRoot supports uncontrolled defaultOpen / controlled open and uncontrolled defaultActiveSnapPoint / controlled activeSnapPoint. |
| Snap |
Snap points are fractions in (0, 1] of the drawer size or "<number>px" visible sizes; invalid values throw VIZE_UI_DRAWER_SNAP_POINT. |
| Styling |
After mount, content publishes --vize-drawer-snap-offset, --vize-drawer-drag-offset, and --vize-drawer-offset (px toward the dismiss edge). |
| Data |
data-vize-ui, part, data-state, data-side, data-modal, data-snap-point, and data-dragging form the styling contract; no CSS ships. |
| SSR |
Server markup never carries the open attribute or offset variables; the dialog is opened only after hydration, so markup hydrates without mismatches. |
| State x input |
Outcome |
Proven by |
| closed x trigger click |
Opens the modal <dialog>, wires aria-labelledby/aria-describedby and trigger aria-controls, focuses inside. |
opens a labelled native modal dialog from the trigger and restores focus on close |
open x DrawerClose click |
Restores focus to the trigger, then closes the native dialog. |
opens a labelled native modal dialog from the trigger and restores focus on close |
open x native cancel event or Escape keydown |
cancel is always prevented and routed through state; dismissal emits dismiss with escape-key. |
Escape and the native cancel event dismiss through Drawer state |
open x prevented escape-key-down, or dismissible=false |
The drawer stays open; the native cancel is still prevented. |
escape-key-down is preventable and non-dismissible drawers ignore cancel |
controlled open x trigger click |
Emits update:open and waits for the parent before calling showModal(). |
controlled open state waits for the parent |
open modal x press on ::backdrop outside the box |
Emits preventable backdrop-pointer-down, then dismisses with reason backdrop; presses inside the box do not. |
backdrop presses outside the dialog box dismiss unless prevented |
| open non-modal x outside press |
Uses show(); an outside pointer-down dismisses through the dismissable layer. |
non-modal drawers use show() and dismiss on outside presses |
open x native close (for example form method="dialog") |
Mirrors the close into state with update:open(false). |
a native close (form method=dialog) is mirrored into state |
| open x drag past the 4px slop |
Sets data-dragging, emits drag-start, publishes drag offsets; release picks the nearest snap point after velocity projection and emits update:activeSnapPoint and drag-end. |
dragging publishes offsets and releases to the nearest snap point |
open x drag past closeThreshold or flick above velocityThreshold |
Dismisses; shorter, slower drags snap back. |
dragging past the threshold or flicking dismisses; short drags snap back |
dismissible=false x long drag; any drag x pointercancel |
Never dismisses; pointercancel resets offsets and emits drag-end with outcome cancel. |
non-dismissible drawers never close from drags and pointercancel restores position |
| tap without movement; click after a drag; drag from form field |
Taps keep their click; the click ending a drag is suppressed; inputs never start drags. |
clicks survive taps but are suppressed after a drag; form fields never start drags |
dragFromContent=false x drag |
Only drags that start on DrawerHandle move or dismiss the drawer. |
dragFromContent=false limits drags to the handle |
DrawerHandle x click / arrow keys |
Click cycles to the next more-visible snap point with wrap; side-aware arrows step without wrapping. |
the handle cycles snap points on click and steps with side-aware arrow keys |
controlled activeSnapPoint x snapTo() |
Emits the request, keeps the parent value, and ignores unknown snap points. |
controlled snap points emit requests and follow the parent |
| invalid snap points; parts without root |
Throw VIZE_UI_DRAWER_SNAP_POINT and VIZE_UI_CONTEXT_MISSING. |
invalid snap points and missing providers throw stable diagnostics |
| snap helpers |
Pixel/fraction offsets, release resolution, and snap stepping are pure and exported. |
snap math resolves pixel and fractional points with velocity projection |
| server render closed and open |
Byte-identical output, no open attribute, no slot contents while closed, no offset variables. |
renders byte-identical closed drawer markup without opening the native dialog |
| hydration of an open drawer |
Reuses the server <dialog>, then calls showModal() and publishes offsets with zero diagnostics. |
renders open drawers deterministically and hydrates before calling showModal |
| public types |
Side, snap point, outcome, and dismiss reason are closed unions; invalid props are rejected. |
src/families/overlays/drawer/drawer.types.test-d.ts |