Drawer

A swipeable panel that slides in from an edge of the screen — dismiss it by swiping, dragging the handle, or tapping outside. Built on Base UI's drawer primitive.

Last updated August 8, 2026

CreditsModifiedPublished

Copied from shadcn/ui and coss ui.

  • Adds a logical position="start"/"end" on top of the physical top/right/bottom/left, resolved from the ambient direction measured at the trigger, and used to derive swipeDirection automatically
  • DrawerMenuTrigger's chevron flips with rtl:-scale-x-100, and menu item padding/checkbox thumb travel use logical pe-/ps- and a rtl:-translate-x flip instead of physical left/right values
  • Guards against a spurious open-then-instant-dismiss: in re-render-heavy apps, the interaction that opens the drawer can produce a follow-up click Base UI reads as an outside-press in the same tick. Any outside-press within 200ms of opening is treated as that ghost press and cancelled
  • Fixed two positioning bugs: justify-[right]/justify-[left] silently failed to generate any CSS (those keywords aren't part of Tailwind's justify-content value scale for the shorthand form), so every non-bottom/top drawer rendered on the physical left regardless of position — replaced with the [justify-content:right]/[justify-content:left] arbitrary-property form. Separately, DrawerPopup reserves space for the drag bar via padding on itself when showBar is set, which inset every child including DrawerFooter's own background — DrawerFooter now cancels that inset with a matching negative margin so its background reaches the handle edge

Overview

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
export function DrawerDemoExample() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline">Open drawer</Button>} />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Edit profile</DrawerTitle>
          <DrawerDescription>
            Make changes to your profile here. Click save when you&apos;re done.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <p className="text-sm text-muted-foreground">
            Swipe down, drag the handle, or tap outside to dismiss.
          </p>
        </DrawerPanel>
        <DrawerFooter>
          <Button type="submit">Save changes</Button>
          <DrawerClose render={<Button variant="outline">Cancel</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

DrawerFooter is optional — skip it for a purely informational sheet that only needs to be swiped away.

import { BadgeCheckIcon } from "lucide-react"
 
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerDescription,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
export function DrawerNoFooterExample() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline">View receipt</Button>} />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Payment successful</DrawerTitle>
          <DrawerDescription>
            Your subscription is active. A receipt was sent to your email.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel className="flex items-center gap-3">
          <BadgeCheckIcon className="size-8 shrink-0 text-primary" />
          <p className="text-sm text-muted-foreground">
            Nothing left to do here — swipe down, drag the handle, or tap
            outside to dismiss.
          </p>
        </DrawerPanel>
      </DrawerPopup>
    </Drawer>
  )
}

Installation

$ npx shadcn@latest add https://ui.persian-labs.ir/r/drawer.json

Usage

import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
export function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline">Open</Button>} />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Edit profile</DrawerTitle>
          <DrawerDescription>
            Make changes to your profile here.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>{/* content */}</DrawerPanel>
        <DrawerFooter>
          <Button type="submit">Save changes</Button>
          <DrawerClose render={<Button variant="outline">Cancel</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

Positions

Pass position to control which edge the drawer slides in from.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
const positions = ["top", "right", "bottom", "left"] as const
const variants = ["default", "inset"] as const
 
export function DrawerPositionsExample() {
  return (
    <div className="flex flex-wrap gap-2">
      {positions.flatMap((position) =>
        variants.map((variant) => (
          <Drawer key={`${position}-${variant}`} position={position}>
            <DrawerTrigger
              render={
                <Button variant="outline" className="capitalize">
                  {position} {variant === "inset" && "(inset)"}
                </Button>
              }
            />
            <DrawerPopup showBar variant={variant}>
              <DrawerHeader>
                <DrawerTitle className="capitalize">
                  {position} {variant} drawer
                </DrawerTitle>
                <DrawerDescription>
                  Slides in from the {position} edge of the screen.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerPanel>
                <p className="text-sm text-muted-foreground">
                  Swipe, drag the handle, or tap outside to dismiss.
                </p>
              </DrawerPanel>
              <DrawerFooter>
                <Button type="submit">Save changes</Button>
                <DrawerClose
                  render={<Button variant="outline">Cancel</Button>}
                />
              </DrawerFooter>
            </DrawerPopup>
          </Drawer>
        ))
      )}
    </div>
  )
}

Variants

default rounds the corners touching the screen edge, straight has no rounding and skips the nested-drawer scale-down effect, and inset floats with a margin from the edge on larger screens.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
const variants = ["default", "straight", "inset"] as const
 
export function DrawerVariantsExample() {
  return (
    <div className="flex flex-wrap gap-2">
      {variants.map((variant) => (
        <Drawer key={variant}>
          <DrawerTrigger
            render={
              <Button variant="outline" className="capitalize">
                {variant}
              </Button>
            }
          />
          <DrawerPopup variant={variant} showBar>
            <DrawerHeader>
              <DrawerTitle className="capitalize">
                {variant} variant
              </DrawerTitle>
              <DrawerDescription>
                {variant === "default" &&
                  "Rounded top corners, flush with the screen edge."}
                {variant === "straight" &&
                  "No rounded corners and no scale-down stacking effect."}
                {variant === "inset" &&
                  "Floats with a margin from the screen edge on larger screens."}
              </DrawerDescription>
            </DrawerHeader>
            <DrawerPanel>
              <p className="text-sm text-muted-foreground">
                Swipe down, drag the handle, or tap outside to dismiss.
              </p>
            </DrawerPanel>
            <DrawerFooter>
              <DrawerClose render={<Button variant="outline">Close</Button>} />
            </DrawerFooter>
          </DrawerPopup>
        </Drawer>
      ))}
    </div>
  )
}

Scrollable content

DrawerHeader and DrawerFooter stay fixed outside the scroll container — DrawerPanel is the only part that scrolls.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
const terms = Array.from({ length: 40 }, (_, i) => `Section ${i + 1}`)
 
export function DrawerScrollableExample() {
  return (
    <Drawer position="right">
      <DrawerTrigger render={<Button variant="outline">View terms</Button>} />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Terms of Service</DrawerTitle>
          <DrawerDescription>
            The header and footer stay fixed — only this panel scrolls.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <div className="flex flex-col gap-3">
            {terms.map((term) => (
              <div key={term} className="text-sm">
                <p className="font-medium text-foreground">{term}</p>
                <p className="text-muted-foreground">
                  Placeholder body text to give this section some height, so the
                  panel actually needs to scroll.
                </p>
              </div>
            ))}
          </div>
        </DrawerPanel>
        <DrawerFooter>
          <Button type="submit">Accept</Button>
          <DrawerClose render={<Button variant="outline">Decline</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

Snap points

Pass snapPoints to have the drawer settle at preset heights instead of following the drag freely. Numbers between 0 and 1 are fractions of the viewport height. Snap points only apply to position="bottom" drawers.

"use client"
 
import * as React from "react"
 
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
const snapPoints = [0.3, 0.6, 1]
const cards = Array.from({ length: 20 }, (_, i) => i + 1)
 
export function DrawerSnapPointsExample() {
  const [snapPoint, setSnapPoint] = React.useState<number | string | null>(
    snapPoints[0] ?? null
  )
 
  return (
    <Drawer
      snapPoints={snapPoints}
      snapPoint={snapPoint}
      onSnapPointChange={setSnapPoint}
    >
      <DrawerTrigger
        render={<Button variant="outline">Open snap drawer</Button>}
      />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Snap points</DrawerTitle>
          <DrawerDescription>
            Drag the handle — it snaps to 30%, 60%, or full height instead of
            following your finger freely. Current snap point:{" "}
            {String(snapPoint)}
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <div className="grid gap-3">
            {cards.map((card) => (
              <div
                key={card}
                className="flex h-12 items-center rounded-md bg-muted px-4 text-sm text-muted-foreground"
              >
                Card {card}
              </div>
            ))}
          </div>
        </DrawerPanel>
        <DrawerFooter>
          <DrawerClose render={<Button variant="outline">Close</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

Nested drawers

A drawer opened from inside another already-open drawer stacks on top of it, scaling the one behind it down slightly. Pass modal="trap-focus" on the inner drawer — the outer one already owns the page-level scroll lock, so the inner one should only trap focus.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
export function DrawerNestedExample() {
  return (
    <Drawer>
      <DrawerTrigger
        render={<Button variant="outline">Delete account</Button>}
      />
      <DrawerPopup showBar variant="inset" className="max-w-sm">
        <DrawerHeader>
          <DrawerTitle>Delete account</DrawerTitle>
          <DrawerDescription>
            This will permanently delete your account and all of its data.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          {/* Nested inside an already-open modal drawer, so it only traps
              focus instead of re-applying a page-level scroll lock on top
              of the outer one. */}
          <Drawer modal="trap-focus">
            <DrawerTrigger
              render={<Button variant="destructive">Delete</Button>}
            />
            <DrawerPopup showBar variant="inset" className="max-w-sm">
              <DrawerHeader>
                <DrawerTitle>Are you absolutely sure?</DrawerTitle>
                <DrawerDescription>
                  This action cannot be undone.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerFooter>
                <Button variant="destructive">Yes, delete it</Button>
                <DrawerClose
                  render={<Button variant="outline">Cancel</Button>}
                />
              </DrawerFooter>
            </DrawerPopup>
          </Drawer>
          <DrawerClose render={<Button variant="outline">Cancel</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

RTL

position="start" opens from the reading-start edge — the right in RTL, the left in LTR — instead of a hardcoded side.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
 
export function DrawerRtlExample() {
  return (
    <Drawer position="start">
      <DrawerTrigger render={<Button variant="outline">باز کردن کشو</Button>} />
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>ویرایش پروفایل</DrawerTitle>
          <DrawerDescription>
            تغییرات را اعمال کنید و روی ذخیره کلیک کنید.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <p className="text-sm text-muted-foreground">
            با کشیدن به سمت شروع صفحه یا کلیک بیرون، کشو بسته می‌شود.
          </p>
        </DrawerPanel>
        <DrawerFooter>
          <Button type="submit">ذخیره تغییرات</Button>
          <DrawerClose render={<Button variant="outline">انصراف</Button>} />
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  )
}

API Reference

Drawer

DrawerPopup