Dots

Drawer

A panel that slides in from a screen edge and can be swiped away.

"use client"

import * as React from "react"

Use a drawer for short, focused tasks on touch screens, such as confirming a transfer or picking a payee. It slides in from an edge, supports swipe to dismiss, snap points and nested drawers. Pick a dialog instead when the task is on a large screen and does not need swipe gestures.

Installation

pnpm dlx shadcn@latest add @dots/drawer

Add the following to your global styles. On iOS Safari, the drawer overlay is absolutely positioned and requires a positioned body to cover the viewport after the page is scrolled.

body {
  position: relative;
}

Usage

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
<Drawer>
  <DrawerTrigger render={<Button variant="outline" />}>Open</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Are you absolutely sure?</DrawerTitle>
      <DrawerDescription>This action cannot be undone.</DrawerDescription>
    </DrawerHeader>
    <div className="p-4">{/* Content here */}</div>
    <DrawerFooter>
      <Button>Submit</Button>
      <DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Composition

Use the following composition to build a Drawer:

Drawer
├── DrawerTrigger
└── DrawerContent
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    └── DrawerFooter

DrawerContent composes the portal, overlay, viewport, and popup. For lower-level control, DrawerPortal, DrawerOverlay, and DrawerSwipeHandle are also exported.

Custom sizes

A vertical drawer sizes itself to its content and is capped at calc(100dvh - 6rem) by default. A side drawer spans 75% of the viewport width, or 24rem on larger screens.

To customize the height of a vertical drawer, use the h-* and max-h-* utilities on DrawerContent.

<DrawerContent className="h-[50vh]">

To customize the width of a side drawer, use the w-* and max-w-* utilities on DrawerContent.

<DrawerContent className="w-96">

When the same component renders in multiple directions, scope an override to one axis using the data-[swipe-axis=*] variants.

<DrawerContent className="data-[swipe-axis=y]:max-h-[50vh] data-[swipe-axis=x]:w-96">

To make a region of the drawer scrollable, make the scroll container a flex item. Avoid h-full, which does not resolve inside a content-sized drawer.

<DrawerContent>
  <DrawerHeader>...</DrawerHeader>
  <div className="flex-1 overflow-y-auto p-4">{/* Scrollable content */}</div>
  <DrawerFooter>...</DrawerFooter>
</DrawerContent>

Styling

The drawer exposes CSS variables for style-level customization. Set the sizing variables on DrawerContent. Set the overlay variable on [data-slot=drawer-overlay] in your CSS.

VariableDefaultDescription
--drawer-inset0pxFloats the drawer from the viewport edges.
--drawer-bleed-backgroundvar(--color-popover)Fills the gap behind the drawer on swipe overshoot.
--drawer-overlay-min-opacity0Minimum overlay opacity. Defaults to 0.5 when snap points are active.

The drawer also sets data attributes you can target with variants such as data-[swipe-direction=down]: on DrawerContent, or group-data-[swipe-axis=y]/drawer-popup: on its descendants.

AttributeValuesSet when
data-swipe-directionup, right, down, leftAlways.
data-swipe-axisx, yAlways.
data-snap-pointsPresentThe drawer has snap points.
data-expandedPresentThe drawer is at the full snap point.
data-swipingPresentA swipe is in progress.
data-nested-drawer-openPresentA nested drawer is open on top.

Position

Use the swipeDirection prop to set the side of the drawer.

Available options are up, right, down, and left.

import { Button } from "@/components/ui/button"
import {
  Drawer,

Swipe handle

Use showSwipeHandle on Drawer to render a swipe handle.

"use client"

import { Button } from "@/components/ui/button"

Nested

Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer.

"use client"

import { useIsMobile } from "@/hooks/use-mobile"

Non-modal

Set modal={false} to allow interaction with the rest of the page while the drawer is open. Combine with disablePointerDismissal to prevent the drawer from closing on outside presses. Use modal="trap-focus" to keep focus inside the drawer while leaving scroll and pointer interaction unrestricted.

import { Button } from "@/components/ui/button"
import {
  Drawer,

Snap points

Use snapPoints to snap a drawer to preset heights. Numbers between 0 and 1 represent fractions of the viewport. Numbers greater than 1 are treated as pixel values. String values support px and rem units. Snap points apply to vertical drawers.

Track the active snap point with the controlled snapPoint and onSnapPointChange props. At the full snap point, the drawer gets a data-expanded attribute you can style with the data-expanded: variant.

"use client"

import { Button } from "@/components/ui/button"

Responsive

You can combine the Dialog and Drawer components to create a responsive dialog. This renders a Dialog component on desktop and a Drawer on mobile.

"use client"

import * as React from "react"

Accessibility

  • The drawer renders as a dialog. Focus moves into it on open, stays inside while it is modal and returns to the trigger on close.
  • Escape closes the drawer. Outside press closes it unless disablePointerDismissal is set; swipe always can.
  • Always render a DrawerTitle. Add a DrawerDescription when the title alone does not explain the task.
  • DrawerSwipeHandle is hidden from assistive technology. Keep a visible close button for people who cannot swipe.

API Reference

Drawer

The root. Holds open state, direction and snap points. Other props pass through to the underlying drawer root.

PropTypeDefault
swipeDirection"up" | "right" | "down" | "left""down"
modalboolean | "trap-focus"true
showSwipeHandlebooleanfalse
snapPoints(number | string)[]-
snapPointnumber | string | null-
defaultSnapPointnumber | string | null-
onSnapPointChange(snapPoint: number | string | null, eventDetails) => void-
snapToSequentialPointsbooleanfalse
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean, eventDetails) => void-
onOpenChangeComplete(open: boolean) => void-
disablePointerDismissalbooleanfalse
actionsRefRefObject<{ unmount: () => void; close: () => void } | null>-

DrawerTrigger and DrawerClose

Buttons that open and close the drawer. Use render to swap in your own element, for example render={<Button variant="outline" />}.

DrawerContent

Renders the portal, overlay (when modal is true), viewport and popup. Accepts className and popup props such as initialFocus and finalFocus.

DrawerHeader, DrawerFooter

Plain div wrappers for the title block and the actions. Accept all div props.

DrawerTitle, DrawerDescription

Accessible name and description for the drawer. Accept className and heading or paragraph props.

DrawerPortal, DrawerOverlay, DrawerSwipeHandle

Lower-level parts for custom layouts. DrawerContent already renders them.