{"slug":"drawer","title":"Drawer","description":"Using the drawer machine in your project.","contentType":"component","framework":"react","content":"A drawer is a panel that slides up from the bottom of the screen, often used in\nmobile applications to present additional content or actions.\n\n## Resources\n\n\n[Latest version: v1.41.1](https://www.npmjs.com/package/@zag-js/drawer)\n[Logic Visualizer](https://zag-visualizer.vercel.app/drawer)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/drawer)\n\n\n\n**Features**\n\n- Supports modal and non-modal modes\n- Focus is trapped and scrolling is blocked in modal mode\n- Supports customizable snap points for partial or full expansion\n- Allows dragging to adjust the sheet's height or swipe to dismiss\n- Provides screen reader announcements via rendered title\n- Pressing `Esc` closes the drawer\n- Supports touch gestures for smooth interaction on mobile devices\n- Supports multiple triggers sharing a single drawer instance\n\n## Installation\n\nInstall the drawer package:\n\n```bash\nnpm install @zag-js/drawer @zag-js/react\n# or\nyarn add @zag-js/drawer @zag-js/react\n```\n\n## Anatomy\n\nTo use the drawer component correctly, you'll need to understand its anatomy and\nhow we name its parts.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the drawer package:\n\n```tsx\nimport * as drawer from \"@zag-js/drawer\"\n```\n\nThe drawer package exports two key functions:\n\n- `machine` — State machine logic.\n- `connect` — Maps machine state to JSX props and event handlers.\n\n> Pass a unique `id` to `useMachine` so generated element ids stay predictable.\n\nThen use the framework integration helpers:\n\n```tsx\nimport * as drawer from \"@zag-js/drawer\"\nimport { useMachine, normalizeProps } from \"@zag-js/react\"\nimport { useId } from \"react\"\n\nexport function Drawer() {\n  const service = useMachine(drawer.machine, { id: useId() })\n\n  const api = drawer.connect(service, normalizeProps)\n\n  return (\n    <>\n      <button {...api.getTriggerProps()}>Open Drawer</button>\n      <div {...api.getBackdropProps()} />\n      <div {...api.getPositionerProps()}>\n        <div {...api.getContentProps()}>\n          <div {...api.getGrabberProps()}>\n            <div {...api.getGrabberIndicatorProps()} />\n          </div>\n          <div>\n            <div {...api.getTitleProps()}>Add New Contact</div>\n            <label>\n              <span>Name</span>\n              <input type=\"text\" />\n            </label>\n            <label>\n              <span>Email</span>\n              <input type=\"email\" />\n            </label>\n            <div>\n              <button>Add Contact</button>\n              <button onClick={() => api.setOpen(false)}>Cancel</button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  )\n}\n```\n\n### Controlled drawer\n\nTo control the drawer's open state, pass the `open` and `onOpenChange`\nproperties.\n\n```tsx\nimport { useState } from \"react\"\n\nexport function ControlledDrawer() {\n  const [open, setOpen] = useState(false)\n\n  const service = useMachine(drawer.machine, {\n    open,\n    onOpenChange(details) {\n      setOpen(details.open)\n    },\n  })\n\n  return (\n    // ...\n  )\n}\n```\n\n### Listening for open state changes\n\nWhen the drawer state changes, the `onOpenChange` callback is invoked.\n\n```tsx {3-7}\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  onOpenChange(details) {\n    // details => { open: boolean }\n    console.log(\"drawer open:\", details.open)\n  },\n})\n```\n\n### Configuring snap points\n\nThe drawer supports customizable snap points, which determine the heights at\nwhich the sheet can \"snap\" when dragged. Snap points can be defined as numbers\n(representing percentages of the content height, e.g., `0.5` for 50%) or pixel\nvalues (e.g., `\"200px\"`). By default, it snaps to full sheet height (`1`).\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  snapPoints: [0.3, 0.5, 1], // 30%, 50%, and 100% of sheet height\n  defaultSnapPoint: 0.5, // Start at 50% height\n  onSnapPointChange: ({ snapPoint }) => {\n    console.log(\"Active snap point:\", snapPoint)\n  },\n})\n```\n\n### Controlled snap point\n\nUse `snapPoint` and `onSnapPointChange` to control the active snap point.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  snapPoints: [0.3, 0.5, 1],\n  snapPoint,\n  onSnapPointChange(details) {\n    setSnapPoint(details.snapPoint)\n  },\n})\n```\n\n### Swipe-to-dismiss behavior\n\nThe drawer can be dismissed by swiping it down quickly or dragging it below a\nthreshold. Customize this behavior with the following properties:\n\n- `swipeVelocityThreshold`: The minimum velocity (in pixels per second) required\n  to dismiss on swipe (default: `700`).\n- `closeThreshold`: The percentage of content height below which the sheet\n  closes when dragged (default: `0.25`).\n\n```tsx {3-4}\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  swipeVelocityThreshold: 800,\n  closeThreshold: 0.3,\n})\n```\n\n### Choosing swipe direction\n\nSet `swipeDirection` to control where the drawer can be dismissed.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  swipeDirection: \"down\",\n})\n```\n\n### Sequential snap points\n\nSet `snapToSequentialPoints` to force dragging through snap points in order.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  snapPoints: [0.25, 0.5, 1],\n  snapToSequentialPoints: true,\n})\n```\n\n### Preventing dragging during scroll\n\nBy default, dragging is prevented if the content inside the drawer is scrollable\nand not at the top or bottom. To allow dragging even during scrolling, set\n`preventDragOnScroll` to `false`:\n\n```tsx {3}\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  preventDragOnScroll: false,\n})\n```\n\n### Non-modal drawer\n\nSet `modal` to `false` to allow interaction behind the drawer.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  modal: false,\n})\n```\n\n### Swipe area\n\nUse `getSwipeAreaProps` to render a region outside the drawer that users can\nswipe to open it (for example, an edge swipe gesture on mobile). The swipe area\nis automatically disabled while the drawer is already open.\n\n```tsx\n<div {...api.getSwipeAreaProps()}>\n  {/* invisible hit area along the screen edge */}\n</div>\n```\n\nYou can customize the swipe area's direction and disabled state:\n\n```tsx\n// Disable the swipe area\napi.getSwipeAreaProps({ disabled: true })\n\n// Override the open direction (defaults to opposite of drawer's swipeDirection)\napi.getSwipeAreaProps({ swipeDirection: \"up\" })\n```\n\n### Multiple triggers\n\nA single drawer instance can be shared across multiple trigger elements. Pass a\n`value` to `getTriggerProps` to identify each trigger.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  onTriggerValueChange({ value }) {\n    // value is the id of the trigger that activated the drawer\n    console.log(\"active trigger:\", value)\n  },\n})\n\nconst api = drawer.connect(service, normalizeProps)\n\nreturn (\n  <>\n    <button {...api.getTriggerProps({ value: \"settings\" })}>Settings</button>\n    <button {...api.getTriggerProps({ value: \"profile\" })}>Profile</button>\n    {/* single shared drawer */}\n    <div {...api.getBackdropProps()} />\n    <div {...api.getPositionerProps()}>\n      <div {...api.getContentProps()}>{/* ... */}</div>\n    </div>\n  </>\n)\n```\n\nWhen the drawer is open and a different trigger is activated, it switches without\nclosing. Focus returns to the correct trigger on close.\n\n### Content draggable\n\nBy default the drawer content itself can be dragged to dismiss or snap. To\nrestrict dragging to only the grabber, pass `draggable: false` to\n`getContentProps`.\n\n```tsx\n<div {...api.getContentProps({ draggable: false })}>\n  <div {...api.getGrabberProps()}>\n    <div {...api.getGrabberIndicatorProps()} />\n  </div>\n  {/* Content here is not draggable */}\n</div>\n```\n\n### Using drawer stack\n\nWhen your app shows multiple drawers that stack on top of each other (with\nindent and background effects), use `createStack` and `connectStack` to\ncoordinate them.\n\n```tsx\nimport * as drawer from \"@zag-js/drawer\"\n\n// Create a shared stack (once, at the app level)\nconst stack = drawer.createStack()\n```\n\nPass the `stack` to each drawer machine:\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  stack,\n})\n```\n\nSubscribe to the stack snapshot and connect it to render indent layers:\n\n```tsx\nconst snapshot = useSyncExternalStore(stack.subscribe, stack.getSnapshot)\nconst stackApi = drawer.connectStack(snapshot, normalizeProps)\n\nreturn (\n  <div {...stackApi.getIndentProps()}>\n    <div {...stackApi.getIndentBackgroundProps()} />\n    {/* drawer instances */}\n  </div>\n)\n```\n\nThe indent element exposes `data-active` / `data-inactive` attributes and the\nCSS variables `--drawer-swipe-progress` and `--drawer-frontmost-height` for\nstyling.\n\n### Closing with Escape\n\nSet `closeOnEscape` to `false` if the drawer should not close on `Esc`.\n\n```tsx\nconst service = useMachine(drawer.machine, {\n  id: useId(),\n  closeOnEscape: false,\n})\n```\n\n### Reading drawer state\n\nThe `api` provides methods to read the drawer's current position:\n\n- `api.getOpenPercentage()` — returns a value between `0` (fully closed) and `1`\n  (fully open at the current snap point).\n- `api.getSnapPointIndex()` — returns the index of the active snap point in the\n  `snapPoints` array, or `-1` if none matches.\n- `api.getContentSize()` — returns the drawer content size in pixels (along the\n  swipe axis), or `null` if not yet measured.\n\n```tsx\nconst percentage = api.getOpenPercentage()\nconst index = api.getSnapPointIndex()\nconst size = api.getContentSize()\n```\n\n## Styling guide\n\nEach part includes a `data-part` attribute you can target in CSS.\n\n### Open and closed state\n\nThe drawer exposes `data-state` attributes you can use for open/closed styles:\n\n```css\n[data-part=\"content\"][data-state=\"open|closed\"] {\n  /* styles for the content in different states */\n}\n\n[data-part=\"trigger\"][data-state=\"open|closed\"] {\n  /* styles for the trigger in different states */\n}\n```\n\n### Swipe and drag state\n\nTarget active swipe or drag interactions:\n\n```css\n[data-part=\"content\"][data-swiping] {\n  /* during any swipe or drag interaction */\n}\n\n[data-part=\"content\"][data-dragging] {\n  /* during drag only (not swipe-to-open) */\n}\n\n[data-part=\"content\"][data-expanded] {\n  /* when at the fully expanded snap point */\n}\n\n[data-part=\"content\"][data-swipe-direction] {\n  /* physical swipe direction: \"up\" | \"down\" | \"left\" | \"right\" */\n}\n\n[data-part=\"backdrop\"][data-swiping] {\n  /* backdrop during swipe or drag */\n}\n\n[data-part=\"swipeArea\"][data-disabled] {\n  /* when swipe area is disabled */\n}\n\n[data-part=\"swipeArea\"][data-swipe-direction] {\n  /* physical open direction */\n}\n```\n\n### Animating the drawer\n\nThe content element uses\n`transform: translate3d(var(--drawer-translate-x, 0px), var(--drawer-translate-y, 0px), 0)`\nby default. Reference these variables in your keyframes:\n\n```css\n@keyframes slideIn {\n  from {\n    transform: translate3d(0, 100%, 0);\n  }\n  to {\n    transform: translate3d(\n      var(--drawer-translate-x, 0px),\n      var(--drawer-translate-y, 0px),\n      0\n    );\n  }\n}\n\n@keyframes slideOut {\n  from {\n    transform: translate3d(\n      var(--drawer-translate-x, 0px),\n      var(--drawer-translate-y, 0px),\n      0\n    );\n  }\n  to {\n    transform: translate3d(0, 100%, 0);\n  }\n}\n\n[data-scope=\"drawer\"][data-part=\"content\"][data-state=\"open\"] {\n  animation: slideIn 500ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n[data-scope=\"drawer\"][data-part=\"content\"][data-state=\"closed\"] {\n  animation: slideOut 500ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n```\n\n### Animation CSS variables\n\nThe following CSS variables are set on the content element:\n\n| Variable                       | Description                      |\n| ------------------------------ | -------------------------------- |\n| `--drawer-translate-x`         | Horizontal translate value       |\n| `--drawer-translate-y`         | Vertical translate value         |\n| `--drawer-snap-point-offset-x` | Snap point offset on the x-axis  |\n| `--drawer-snap-point-offset-y` | Snap point offset on the y-axis  |\n| `--drawer-swipe-movement-x`    | Current swipe movement on x-axis |\n| `--drawer-swipe-movement-y`    | Current swipe movement on y-axis |\n| `--drawer-swipe-strength`      | Numeric swipe strength           |\n\nThe following CSS variables are set on the backdrop element:\n\n| Variable                  | Description                                     |\n| ------------------------- | ----------------------------------------------- |\n| `--drawer-swipe-progress` | 0-1 progress value, useful for backdrop opacity |\n| `--drawer-swipe-strength` | Numeric swipe strength                          |\n\n## Methods and Properties\n\nThe drawer's `api` exposes the following methods and properties:\n\n### Machine Context\n\nThe drawer machine exposes the following context properties:\n\n**`ids`**\nType: `ElementIds | undefined`\nDescription: The ids of the elements in the drawer. Useful for composition.\n\n**`trapFocus`**\nType: `boolean | undefined`\nDescription: Whether to trap focus inside the sheet when it's opened.\n\n**`preventScroll`**\nType: `boolean | undefined`\nDescription: Whether to prevent scrolling behind the sheet when it's opened\n\n**`modal`**\nType: `boolean | undefined`\nDescription: Whether to prevent pointer interaction outside the element and hide all content below it.\n\n**`initialFocusEl`**\nType: `(() => MaybeElement) | undefined`\nDescription: Element to receive focus when the sheet is opened.\n\n**`finalFocusEl`**\nType: `(() => MaybeElement) | undefined`\nDescription: Element to receive focus when the sheet is closed.\n\n**`restoreFocus`**\nType: `boolean | undefined`\nDescription: Whether to restore focus to the element that had focus before the sheet was opened.\n\n**`role`**\nType: `\"dialog\" | \"alertdialog\" | undefined`\nDescription: The sheet's role\n\n**`triggerValue`**\nType: `string | null | undefined`\nDescription: The value of the trigger that currently controls the drawer.\n\n**`defaultTriggerValue`**\nType: `string | null | undefined`\nDescription: The default trigger value (uncontrolled).\n\n**`onTriggerValueChange`**\nType: `((details: TriggerValueChangeDetails) => void) | undefined`\nDescription: Callback when the active trigger value changes.\n\n**`open`**\nType: `boolean | undefined`\nDescription: Whether the drawer is open.\n\n**`defaultOpen`**\nType: `boolean | undefined`\nDescription: The initial open state of the drawer.\n\n**`onOpenChange`**\nType: `((details: OpenChangeDetails) => void) | undefined`\nDescription: Function called when the open state changes.\n\n**`closeOnInteractOutside`**\nType: `boolean | undefined`\nDescription: Whether to close the drawer when the outside is clicked.\n\n**`closeOnEscape`**\nType: `boolean | undefined`\nDescription: Whether to close the drawer when the escape key is pressed.\n\n**`snapPoints`**\nType: `SnapPoint[] | undefined`\nDescription: The snap points of the drawer.\nArray of numbers or strings representing the snap points.\n\n**`swipeDirection`**\nType: `SwipeDirection | undefined`\nDescription: The direction in which the drawer can be swiped.\n\n**`snapToSequentialPoints`**\nType: `boolean | undefined`\nDescription: Whether the drawer should snap to sequential points when swiping.\n\n**`swipeVelocityThreshold`**\nType: `number | undefined`\nDescription: The threshold velocity (in pixels/s) for closing the drawer.\n\n**`closeThreshold`**\nType: `number | undefined`\nDescription: The threshold distance for dismissing the drawer.\n\n**`preventDragOnScroll`**\nType: `boolean | undefined`\nDescription: Whether to prevent dragging on scrollable elements.\nWhen enabled, the sheet will not start dragging if the user is interacting with a scrollable element.\n\n**`snapPoint`**\nType: `SnapPoint | null | undefined`\nDescription: The currently active snap point.\n\n**`defaultSnapPoint`**\nType: `SnapPoint | null | undefined`\nDescription: The default snap point of the drawer.\n\n**`onSnapPointChange`**\nType: `((details: SnapPointChangeDetails) => void) | undefined`\nDescription: Callback fired when the snap point changes.\n\n**`stack`**\nType: `DrawerStack | undefined`\nDescription: Optional external store for coordinating app-level drawer stack visuals\n(e.g. indent and background layers).\n\n**`dir`**\nType: `\"ltr\" | \"rtl\" | undefined`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `(() => ShadowRoot | Document | Node) | undefined`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n### Machine API\n\nThe drawer `api` exposes the following methods:\n\n**`open`**\nType: `boolean`\nDescription: Whether the drawer is open.\n\n**`dragging`**\nType: `boolean`\nDescription: Whether the drawer is currently being dragged.\n\n**`triggerValue`**\nType: `string | null`\nDescription: The value of the active trigger.\n\n**`setTriggerValue`**\nType: `(value: string | null) => void`\nDescription: Set the active trigger value.\n\n**`setOpen`**\nType: `(open: boolean) => void`\nDescription: Function to open or close the menu.\n\n**`snapPoints`**\nType: `SnapPoint[]`\nDescription: The snap points of the drawer.\n\n**`swipeDirection`**\nType: `SwipeDirection`\nDescription: The swipe direction of the drawer.\n\n**`snapPoint`**\nType: `SnapPoint | null`\nDescription: The currently active snap point.\n\n**`setSnapPoint`**\nType: `(snapPoint: SnapPoint | null) => void`\nDescription: Function to set the active snap point.\n\n**`getOpenPercentage`**\nType: `() => number`\nDescription: Get the current open percentage of the drawer.\n\n**`getSnapPointIndex`**\nType: `() => number`\nDescription: Get the index of the currently active snap point.\n\n**`getContentSize`**\nType: `() => number | null`\nDescription: Get the current main-axis size of the drawer content.\n\n### Data Attributes\n\n**`Positioner`**\n\n**`data-scope`**: drawer\n**`data-part`**: positioner\n**`data-state`**: \"open\" | \"closed\"\n**`data-swipe-direction`**: \n\n**`Content`**\n\n**`data-scope`**: drawer\n**`data-part`**: content\n**`data-state`**: \"open\" | \"closed\"\n**`data-expanded`**: Present when expanded\n**`data-swipe-direction`**: \n**`data-swiping`**: \n**`data-dragging`**: Present when in the dragging state\n\n**`Trigger`**\n\n**`data-scope`**: drawer\n**`data-part`**: trigger\n**`data-value`**: The value of the item\n**`data-state`**: \"open\" | \"closed\"\n**`data-current`**: Present when current\n\n**`Backdrop`**\n\n**`data-scope`**: drawer\n**`data-part`**: backdrop\n**`data-state`**: \"open\" | \"closed\"\n**`data-swiping`**: \n\n**`SwipeArea`**\n\n**`data-scope`**: drawer\n**`data-part`**: swipe-area\n**`data-state`**: \"open\" | \"closed\"\n**`data-swiping`**: \n**`data-swipe-direction`**: \n**`data-disabled`**: Present when disabled\n\n### CSS Variables\n\n<CssVarTable name=\"drawer\" />","package":"@zag-js/drawer","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/drawer.mdx"}