"use client"
import * as React from "react"
import { toast } from "sonner"
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
FieldTitle,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
const deliveryTimes = [
{
value: "asap",
id: "delivery-asap",
label: "Standard delivery",
description: "25–35 min · Driver assigned now",
badge: "Fastest",
},
{
value: "5-00",
id: "delivery-5-00",
label: "5:00 PM – 5:15 PM",
description: "Prep starts at 4:45 PM",
},
{
value: "5-30",
id: "delivery-5-30",
label: "5:30 PM – 5:45 PM",
description: "Good if you're heading home",
},
{
value: "6-00",
id: "delivery-6-00",
label: "6:00 PM – 6:15 PM",
description: "Most popular · High demand",
},
{
value: "6-30",
id: "delivery-6-30",
label: "6:30 PM – 6:45 PM",
description: "Last slot before kitchen closes",
},
]
export function DrawerDemo() {
const [open, setOpen] = React.useState(false)
const [deliveryTime, setDeliveryTime] = React.useState("asap")
const isMobile = useIsMobile()
function handleConfirm() {
const selected = deliveryTimes.find((time) => time.value === deliveryTime)
if (!selected) {
return
}
setOpen(false)
toast("Delivery time confirmed", {
description: selected.label,
})
}
return (
<Drawer
open={open}
onOpenChange={setOpen}
showSwipeHandle={isMobile}
swipeDirection={isMobile ? "down" : "right"}
>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Pick a delivery time</DrawerTitle>
<DrawerDescription>
We'll prepare your order as soon as possible.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 scroll-fade overflow-y-auto p-4">
<RadioGroup
value={deliveryTime}
onValueChange={setDeliveryTime}
className="gap-2"
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value} htmlFor={time.id}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle className="flex items-center gap-2">
{time.label}
{time.badge ? (
<Badge variant="secondary">{time.badge}</Badge>
) : null}
</FieldTitle>
<FieldDescription>{time.description}</FieldDescription>
</FieldContent>
<RadioGroupItem value={time.value} id={time.id} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button onClick={handleConfirm} className="h-[34px]">
Confirm Delivery Time
</Button>
<DrawerClose render={<Button variant="outline" />}>
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Installation
npx shadcn@latest add @fulldev/drawer
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
Custom Sizes
A vertical drawer sizes itself to its content and is capped at calc(100dvh - 6rem) by default.
<DrawerContent className="h-[50vh]">
Position
Use the swipeDirection prop to set the side of the drawer.
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
export function DrawerWithSides() {
return (
<Drawer swipeDirection="left">
<DrawerTrigger render={<Button variant="secondary" />}>
Open Left Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="size-full rounded-2xl bg-muted" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Swipe Handle
Use showSwipeHandle on Drawer to render a swipe handle.
"use client"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
export function DrawerSwipeHandle() {
return (
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>Drawer with a swipe handle.</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Nested
Open drawers from inside another drawer.
"use client"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
export function DrawerNested() {
const isMobile = useIsMobile()
const swipeDirection = isMobile ? "down" : "right"
return (
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>
Open another drawer from the same direction.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button variant="outline" />}>
Open Nested Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Nested Drawer</DrawerTitle>
<DrawerDescription>
The parent drawer stays mounted behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button variant="outline" />}>
Open Third Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Third Drawer</DrawerTitle>
<DrawerDescription>
Two drawers are stacked behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button variant="outline" />}>
Open Fourth Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Fourth Drawer</DrawerTitle>
<DrawerDescription>
This is the frontmost drawer in the stack.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Non Modal
Set modal={false} to allow interaction with the rest of the page while the drawer is open.
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
export function DrawerNonModal() {
return (
<Drawer modal={false} disablePointerDismissal swipeDirection="right">
<DrawerTrigger render={<Button variant="outline" />}>
Non Modal
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Non Modal Drawer</DrawerTitle>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Snap Points
Use snapPoints to snap a drawer to preset heights.
"use client"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
const SNAP_POINTS = ["31rem", 1]
export function DrawerSnapPoints() {
return (
<Drawer snapPoints={SNAP_POINTS} showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
Open Snap Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Snap points</DrawerTitle>
<DrawerDescription>
Drag the drawer to snap between a compact peek and a near
full-height view.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Responsive
You can combine the Dialog and Drawer components to create a responsive dialog.
"use client"
import * as React from "react"
import { cn } from "cn"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function DrawerDialogDemo() {
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button variant="outline" />}>
Edit Profile
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're
done.
</DialogDescription>
</DialogHeader>
<ProfileForm />
</DialogContent>
</Dialog>
)
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger render={<Button variant="outline" />}>
Edit Profile
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Edit profile</DrawerTitle>
<DrawerDescription>
Make changes to your profile here. Click save when you're done.
</DrawerDescription>
</DrawerHeader>
<ProfileForm className="p-4" />
</DrawerContent>
</Drawer>
)
}
function ProfileForm({ className }: React.ComponentProps<"form">) {
return (
<form className={cn("grid items-start gap-6", className)}>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input type="email" id="email" defaultValue="shadcn@example.com" />
</div>
<div className="grid gap-3">
<Label htmlFor="username">Username</Label>
<Input id="username" defaultValue="@shadcn" />
</div>
<Button type="submit">Save changes</Button>
</form>
)
}Migrating from Vaul
The base drawer now uses Base UI instead of Vaul.
- npm install vaul
+ npm install @base-ui/react
API Reference
Source: react/src/components/ui/drawer.tsx
See the Base UI documentation for the full API reference.