Skip to main content

A drawer is a panel that slides up from the bottom of the screen, often used in mobile applications to present additional content or actions.

Loading...

Features

  • Supports modal and non-modal modes
  • Focus is trapped and scrolling is blocked in modal mode
  • Supports customizable snap points for partial or full expansion
  • Allows dragging to adjust the sheet's height or swipe to dismiss
  • Provides screen reader announcements via rendered title
  • Pressing Esc closes the drawer
  • Supports touch gestures for smooth interaction on mobile devices
  • Supports multiple triggers sharing a single drawer instance

Installation

Install the drawer package:

npm install @zag-js/drawer @zag-js/react # or yarn add @zag-js/drawer @zag-js/react

Anatomy

To use the drawer component correctly, you'll need to understand its anatomy and how we name its parts.

Each part includes a data-part attribute to help identify them in the DOM.

No anatomy available for drawer

Usage

Import the drawer package:

import * as drawer from "@zag-js/drawer"

The drawer package exports two key functions:

  • machine — State machine logic.
  • connect — Maps machine state to JSX props and event handlers.

Pass a unique id to useMachine so generated element ids stay predictable.

Then use the framework integration helpers:

import * as drawer from "@zag-js/drawer" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" export function Drawer() { const service = useMachine(drawer.machine, { id: useId() }) const api = drawer.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps()}>Open Drawer</button> <div {...api.getBackdropProps()} /> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getGrabberProps()}> <div {...api.getGrabberIndicatorProps()} /> </div> <div> <div {...api.getTitleProps()}>Add New Contact</div> <label> <span>Name</span> <input type="text" /> </label> <label> <span>Email</span> <input type="email" /> </label> <div> <button>Add Contact</button> <button onClick={() => api.setOpen(false)}>Cancel</button> </div> </div> </div> </div> </> ) }

Controlled drawer

To control the drawer's open state, pass the open and onOpenChange properties.

import { useState } from "react" export function ControlledDrawer() { const [open, setOpen] = useState(false) const service = useMachine(drawer.machine, { open, onOpenChange(details) { setOpen(details.open) }, }) return ( // ... ) }

Listening for open state changes

When the drawer state changes, the onOpenChange callback is invoked.

const service = useMachine(drawer.machine, { id: useId(), onOpenChange(details) { // details => { open: boolean } console.log("drawer open:", details.open) }, })

Configuring snap points

The drawer supports customizable snap points, which determine the heights at which the sheet can "snap" when dragged. Snap points can be defined as numbers (representing percentages of the content height, e.g., 0.5 for 50%) or pixel values (e.g., "200px"). By default, it snaps to full sheet height (1).

const service = useMachine(drawer.machine, { id: useId(), snapPoints: [0.3, 0.5, 1], // 30%, 50%, and 100% of sheet height defaultSnapPoint: 0.5, // Start at 50% height onSnapPointChange: ({ snapPoint }) => { console.log("Active snap point:", snapPoint) }, })

Controlled snap point

Use snapPoint and onSnapPointChange to control the active snap point.

const service = useMachine(drawer.machine, { id: useId(), snapPoints: [0.3, 0.5, 1], snapPoint, onSnapPointChange(details) { setSnapPoint(details.snapPoint) }, })

Swipe-to-dismiss behavior

The drawer can be dismissed by swiping it down quickly or dragging it below a threshold. Customize this behavior with the following properties:

  • swipeVelocityThreshold: The minimum velocity (in pixels per second) required to dismiss on swipe (default: 700).
  • closeThreshold: The percentage of content height below which the sheet closes when dragged (default: 0.25).
const service = useMachine(drawer.machine, { id: useId(), swipeVelocityThreshold: 800, closeThreshold: 0.3, })

Choosing swipe direction

Set swipeDirection to control where the drawer can be dismissed.

const service = useMachine(drawer.machine, { id: useId(), swipeDirection: "down", })

Sequential snap points

Set snapToSequentialPoints to force dragging through snap points in order.

const service = useMachine(drawer.machine, { id: useId(), snapPoints: [0.25, 0.5, 1], snapToSequentialPoints: true, })

Preventing dragging during scroll

By default, dragging is prevented if the content inside the drawer is scrollable and not at the top or bottom. To allow dragging even during scrolling, set preventDragOnScroll to false:

const service = useMachine(drawer.machine, { id: useId(), preventDragOnScroll: false, })

Non-modal drawer

Set modal to false to allow interaction behind the drawer.

const service = useMachine(drawer.machine, { id: useId(), modal: false, })

Swipe area

Use getSwipeAreaProps to render a region outside the drawer that users can swipe to open it (for example, an edge swipe gesture on mobile). The swipe area is automatically disabled while the drawer is already open.

<div {...api.getSwipeAreaProps()}> {/* invisible hit area along the screen edge */} </div>

You can customize the swipe area's direction and disabled state:

// Disable the swipe area api.getSwipeAreaProps({ disabled: true }) // Override the open direction (defaults to opposite of drawer's swipeDirection) api.getSwipeAreaProps({ swipeDirection: "up" })

Multiple triggers

A single drawer instance can be shared across multiple trigger elements. Pass a value to getTriggerProps to identify each trigger.

const service = useMachine(drawer.machine, { id: useId(), onTriggerValueChange({ value }) { // value is the id of the trigger that activated the drawer console.log("active trigger:", value) }, }) const api = drawer.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps({ value: "settings" })}>Settings</button> <button {...api.getTriggerProps({ value: "profile" })}>Profile</button> {/* single shared drawer */} <div {...api.getBackdropProps()} /> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}>{/* ... */}</div> </div> </> )

When the drawer is open and a different trigger is activated, it switches without closing. Focus returns to the correct trigger on close.

Content draggable

By default the drawer content itself can be dragged to dismiss or snap. To restrict dragging to only the grabber, pass draggable: false to getContentProps.

<div {...api.getContentProps({ draggable: false })}> <div {...api.getGrabberProps()}> <div {...api.getGrabberIndicatorProps()} /> </div> {/* Content here is not draggable */} </div>

Using drawer stack

When your app shows multiple drawers that stack on top of each other (with indent and background effects), use createStack and connectStack to coordinate them.

import * as drawer from "@zag-js/drawer" // Create a shared stack (once, at the app level) const stack = drawer.createStack()

Pass the stack to each drawer machine:

const service = useMachine(drawer.machine, { id: useId(), stack, })

Subscribe to the stack snapshot and connect it to render indent layers:

const snapshot = useSyncExternalStore(stack.subscribe, stack.getSnapshot) const stackApi = drawer.connectStack(snapshot, normalizeProps) return ( <div {...stackApi.getIndentProps()}> <div {...stackApi.getIndentBackgroundProps()} /> {/* drawer instances */} </div> )

The indent element exposes data-active / data-inactive attributes and the CSS variables --drawer-swipe-progress and --drawer-frontmost-height for styling.

Closing with Escape

Set closeOnEscape to false if the drawer should not close on Esc.

const service = useMachine(drawer.machine, { id: useId(), closeOnEscape: false, })

Reading drawer state

The api provides methods to read the drawer's current position:

  • api.getOpenPercentage() — returns a value between 0 (fully closed) and 1 (fully open at the current snap point).
  • api.getSnapPointIndex() — returns the index of the active snap point in the snapPoints array, or -1 if none matches.
  • api.getContentSize() — returns the drawer content size in pixels (along the swipe axis), or null if not yet measured.
const percentage = api.getOpenPercentage() const index = api.getSnapPointIndex() const size = api.getContentSize()

Styling guide

Each part includes a data-part attribute you can target in CSS.

Open and closed state

The drawer exposes data-state attributes you can use for open/closed styles:

[data-part="content"][data-state="open|closed"] { /* styles for the content in different states */ } [data-part="trigger"][data-state="open|closed"] { /* styles for the trigger in different states */ }

Swipe and drag state

Target active swipe or drag interactions:

[data-part="content"][data-swiping] { /* during any swipe or drag interaction */ } [data-part="content"][data-dragging] { /* during drag only (not swipe-to-open) */ } [data-part="content"][data-expanded] { /* when at the fully expanded snap point */ } [data-part="content"][data-swipe-direction] { /* physical swipe direction: "up" | "down" | "left" | "right" */ } [data-part="backdrop"][data-swiping] { /* backdrop during swipe or drag */ } [data-part="swipeArea"][data-disabled] { /* when swipe area is disabled */ } [data-part="swipeArea"][data-swipe-direction] { /* physical open direction */ }

Animating the drawer

The content element uses transform: translate3d(var(--drawer-translate-x, 0px), var(--drawer-translate-y, 0px), 0) by default. Reference these variables in your keyframes:

@keyframes slideIn { from { transform: translate3d(0, 100%, 0); } to { transform: translate3d( var(--drawer-translate-x, 0px), var(--drawer-translate-y, 0px), 0 ); } } @keyframes slideOut { from { transform: translate3d( var(--drawer-translate-x, 0px), var(--drawer-translate-y, 0px), 0 ); } to { transform: translate3d(0, 100%, 0); } } [data-scope="drawer"][data-part="content"][data-state="open"] { animation: slideIn 500ms cubic-bezier(0.32, 0.72, 0, 1); } [data-scope="drawer"][data-part="content"][data-state="closed"] { animation: slideOut 500ms cubic-bezier(0.32, 0.72, 0, 1); }

Animation CSS variables

The following CSS variables are set on the content element:

VariableDescription
--drawer-translate-xHorizontal translate value
--drawer-translate-yVertical translate value
--drawer-snap-point-offset-xSnap point offset on the x-axis
--drawer-snap-point-offset-ySnap point offset on the y-axis
--drawer-swipe-movement-xCurrent swipe movement on x-axis
--drawer-swipe-movement-yCurrent swipe movement on y-axis
--drawer-swipe-strengthNumeric swipe strength

The following CSS variables are set on the backdrop element:

VariableDescription
--drawer-swipe-progress0-1 progress value, useful for backdrop opacity
--drawer-swipe-strengthNumeric swipe strength

Methods and Properties

The drawer's api exposes the following methods and properties:

Machine Context

The drawer machine exposes the following context properties:

  • idsElementIds | undefinedThe ids of the elements in the drawer. Useful for composition.
  • trapFocusboolean | undefinedWhether to trap focus inside the sheet when it's opened.
  • preventScrollboolean | undefinedWhether to prevent scrolling behind the sheet when it's opened
  • modalboolean | undefinedWhether to prevent pointer interaction outside the element and hide all content below it.
  • initialFocusEl(() => MaybeElement) | undefinedElement to receive focus when the sheet is opened.
  • finalFocusEl(() => MaybeElement) | undefinedElement to receive focus when the sheet is closed.
  • restoreFocusboolean | undefinedWhether to restore focus to the element that had focus before the sheet was opened.
  • role"dialog" | "alertdialog" | undefinedThe sheet's role
  • triggerValuestring | null | undefinedThe value of the trigger that currently controls the drawer.
  • defaultTriggerValuestring | null | undefinedThe default trigger value (uncontrolled).
  • onTriggerValueChange((details: TriggerValueChangeDetails) => void) | undefinedCallback when the active trigger value changes.
  • openboolean | undefinedWhether the drawer is open.
  • defaultOpenboolean | undefinedThe initial open state of the drawer.
  • onOpenChange((details: OpenChangeDetails) => void) | undefinedFunction called when the open state changes.
  • closeOnInteractOutsideboolean | undefinedWhether to close the drawer when the outside is clicked.
  • closeOnEscapeboolean | undefinedWhether to close the drawer when the escape key is pressed.
  • snapPointsSnapPoint[] | undefinedThe snap points of the drawer. Array of numbers or strings representing the snap points.
  • swipeDirectionSwipeDirection | undefinedThe direction in which the drawer can be swiped.
  • snapToSequentialPointsboolean | undefinedWhether the drawer should snap to sequential points when swiping.
  • swipeVelocityThresholdnumber | undefinedThe threshold velocity (in pixels/s) for closing the drawer.
  • closeThresholdnumber | undefinedThe threshold distance for dismissing the drawer.
  • preventDragOnScrollboolean | undefinedWhether to prevent dragging on scrollable elements. When enabled, the sheet will not start dragging if the user is interacting with a scrollable element.
  • snapPointSnapPoint | null | undefinedThe currently active snap point.
  • defaultSnapPointSnapPoint | null | undefinedThe default snap point of the drawer.
  • onSnapPointChange((details: SnapPointChangeDetails) => void) | undefinedCallback fired when the snap point changes.
  • stackDrawerStack | undefinedOptional external store for coordinating app-level drawer stack visuals (e.g. indent and background layers).
  • dir"ltr" | "rtl" | undefinedThe document's text/writing direction.
  • idstringThe unique identifier of the machine.
  • getRootNode(() => ShadowRoot | Document | Node) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

Machine API

The drawer api exposes the following methods:

  • openbooleanWhether the drawer is open.
  • draggingbooleanWhether the drawer is currently being dragged.
  • triggerValuestring | nullThe value of the active trigger.
  • setTriggerValue(value: string | null) => voidSet the active trigger value.
  • setOpen(open: boolean) => voidFunction to open or close the menu.
  • snapPointsSnapPoint[]The snap points of the drawer.
  • swipeDirectionSwipeDirectionThe swipe direction of the drawer.
  • snapPointSnapPoint | nullThe currently active snap point.
  • setSnapPoint(snapPoint: SnapPoint | null) => voidFunction to set the active snap point.
  • getOpenPercentage() => numberGet the current open percentage of the drawer.
  • getSnapPointIndex() => numberGet the index of the currently active snap point.
  • getContentSize() => number | nullGet the current main-axis size of the drawer content.

Data Attributes

Positioner
data-scope
drawer
data-part
positioner
data-state
"open" | "closed"
Content
data-scope
drawer
data-part
content
data-state
"open" | "closed"
data-expanded
Present when expanded
data-dragging
Present when in the dragging state
Trigger
data-scope
drawer
data-part
trigger
data-value
The value of the item
data-state
"open" | "closed"
data-current
Present when current
Backdrop
data-scope
drawer
data-part
backdrop
data-state
"open" | "closed"
SwipeArea
data-scope
drawer
data-part
swipe-area
data-state
"open" | "closed"
data-disabled
Present when disabled

CSS Variables

Content
--drawer-translate
The drawer translate value for the Content
--drawer-translate-x
The drawer translate x value for the Content
--drawer-translate-y
The drawer translate y value for the Content
--drawer-snap-point-offset-x
The offset position for drawer snap point
--drawer-snap-point-offset-y
The offset position for drawer snap point
--drawer-swipe-movement-x
The drawer swipe movement x value for the Content
--drawer-swipe-movement-y
The drawer swipe movement y value for the Content
--drawer-swipe-strength
The drawer swipe strength value for the Content
--layer-index
The index of the dismissable in the layer stack
--nested-layer-count
The number of nested drawers
Backdrop
--drawer-swipe-progress
The drawer swipe progress value for the Backdrop
--drawer-swipe-strength
The drawer swipe strength value for the Backdrop
--layer-index
The index of the dismissable in the layer stack
Edit this page on GitHub