shadcn.io is not affiliated with official shadcn/ui
Installation
bunx --bun shadcn@latest add https://kit.dev/r/radix/drawer.jsonnpx shadcn@latest add https://kit.dev/r/radix/drawer.jsonpnpm dlx shadcn@latest add https://kit.dev/r/radix/drawer.jsonyarn shadcn@latest add https://kit.dev/r/radix/drawer.jsonThis component depends on Button. Install it first if you haven't already.
Install the following dependencies:
bun add vaul tailwind-variantsnpm install vaul tailwind-variantspnpm add vaul tailwind-variantsyarn add vaul tailwind-variantsCopy and paste the following code into your project.
"use client";
import type React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
export const Drawer = (
props: React.ComponentProps<typeof DrawerPrimitive.Root>
) => <DrawerPrimitive.Root data-slot="drawer" {...props} />;
export const DrawerTrigger = (
props: React.ComponentProps<typeof DrawerPrimitive.Trigger>
) => <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
export const DrawerPortal = (
props: React.ComponentProps<typeof DrawerPrimitive.Portal>
) => <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
Update the import paths to match your project setup.
Anatomy
DrawerPositioner
DrawerPositioner
DrawerTitle
DrawerDescription
DrawerTrigger
DrawerOverlay
DrawerGrabber
└── DrawerGrabberIndicator
DrawerClose
DrawerSwipeAreaUsage
import { useState } from "react";
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";
import { toast } from "@/components/ui/toast";
import { useIsMobile } from "@/hooks/use-is-mobile";const deliveryTimes = [
{
badge: "Fastest",
description: "25–35 min · Driver assigned now",
id: "delivery-asap",
label: "Standard delivery",
value: "asap",
},
{
description: "Prep starts at 4:45 PM",
id: "delivery-5-00",
label: "5:00 PM – 5:15 PM",
value: "5-00",
},
{
description: "Good if you're heading home",
id: "delivery-5-30",
label: "5:30 PM – 5:45 PM",
value: "5-30",
},
{
description: "Most popular · High demand",
id: "delivery-6-00",
label: "6:00 PM – 6:15 PM",
value: "6-00",
},
{
description: "Last slot before kitchen closes",
id: "delivery-6-30",
label: "6:30 PM – 6:45 PM",
value: "6-30",
},
];
const DrawerDemo = () => {
const [open, setOpen] = useState(false);
const [deliveryTime, setDeliveryTime] = useState("asap");
const isMobile = useIsMobile();
const handleOpenChange = (details: { open: boolean }) => {
setOpen(details.open);
};
const handleDeliveryTimeChange = (details: { value: string | null }) => {
if (details.value !== null) {
setDeliveryTime(details.value);
}
};
const handleConfirm = () => {
const selected = deliveryTimes.find((time) => time.value === deliveryTime);
if (!selected) {
return;
}
setOpen(false);
toast.create({
description: selected.label,
title: "Delivery time confirmed",
});
};
return (
<Drawer
onOpenChange={handleOpenChange}
open={open}
swipeDirection={isMobile ? "down" : "right"}
>
<DrawerTrigger asChild>
<Button variant="secondary">Open Drawer</Button>
</DrawerTrigger>
<DrawerContent showBar={isMobile}>
<DrawerHeader>
<DrawerTitle>Pick a delivery time</DrawerTitle>
<DrawerDescription>
We'll prepare your order as soon as possible.
</DrawerDescription>
</DrawerHeader>
<div className="scroll-fade flex-1 overflow-y-auto p-4">
<RadioGroup
className="gap-2"
onValueChange={handleDeliveryTimeChange}
value={deliveryTime}
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value}>
<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}>
<span className="sr-only">{time.label}</span>
</RadioGroupItem>
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button className="h-[34px]" onClick={handleConfirm}>
Confirm Delivery Time
</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
};
Accessibility
| Key | Description |
|---|---|
Enter | Activate the focused control. |
Space | Activate the focused control. |
End | Go to the end. |
Demos
Sides
Position the drawer with swipeDirection.
Swipe Handle
Show a swipe handle with showBar on DrawerContent.
Nested
Open drawers from inside another drawer.
Non Modal
Allow page interaction while the drawer is open with modal={false}.
Snap Points
Snap a vertical drawer to preset heights with snapPoints.
Dialog
Combine Dialog and Drawer for a responsive edit form.
API Reference
Drawer
PropType
AttributeType
DrawerTrigger
PropType
AttributeType
DrawerPortal
PropType
AttributeType
DrawerClose
PropType
AttributeType
DrawerOverlay
PropType
AttributeType
DrawerContent
PropType
AttributeType
DrawerHeader
PropType
AttributeType
DrawerFooter
PropType
AttributeType
DrawerTitle
PropType
AttributeType
DrawerDescription
PropType
AttributeType