"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Field, FieldGroup } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function DrawerDemo() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="tertiary">Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Edit profile</DrawerTitle>
<DrawerDescription>
Make changes to your profile here. Click save when you're done.
</DrawerDescription>
</DrawerHeader>
<FieldGroup className="px-4">
<Field>
<Label htmlFor="drawer-name">Name</Label>
<Input
variant="secondary"
id="drawer-name"
defaultValue="Maged Ibrahim"
/>
</Field>
<Field>
<Label htmlFor="drawer-username">Username</Label>
<Input
variant="secondary"
id="drawer-username"
defaultValue="@0xMaqed"
/>
</Field>
</FieldGroup>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="tertiary">Cancel</Button>
</DrawerClose>
<Button type="submit">Save changes</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
pnpm dlx shadcn@latest add @herocn/drawerimport {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Use the following composition to build a Drawer:
Drawer
├── DrawerTrigger
└── DrawerContent
├── DrawerHeader
│ ├── DrawerTitle
│ └── DrawerDescription
└── DrawerFooterUse the direction prop on Drawer to set the side the drawer slides in from. Available options are top, right, bottom, and left.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const DRAWER_SIDES = ["top", "right", "bottom", "left"] as const;
export function DrawerDirection() {
return (
<div className="flex flex-wrap gap-2">
{DRAWER_SIDES.map((side) => (
<Drawer
key={side}
direction={
side === "bottom" ? undefined : (side as "top" | "right" | "left")
}
>
<DrawerTrigger asChild>
<Button variant="tertiary" className="capitalize">
{side}
</Button>
</DrawerTrigger>
<DrawerContent className="data-[vaul-drawer-direction=bottom]:max-h-[50vh] data-[vaul-drawer-direction=top]:max-h-[50vh]">
<DrawerHeader>
<DrawerTitle>Terms of Service</DrawerTitle>
<DrawerDescription>
Make sure to read them carefully.
</DrawerDescription>
</DrawerHeader>
<div className="no-scrollbar overflow-y-auto px-4">
{Array.from({ length: 10 }).map((_, index) => (
<p key={index} className="mb-4 leading-relaxed">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
))}
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="tertiary">Cancel</Button>
</DrawerClose>
<Button type="submit">Accept</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
))}
</div>
);
}
Use overlayVariant on DrawerContent to choose an opaque dim (opaque), a blurred backdrop (blur), or transparent overlay (transparent).
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const variants = [
{
variant: "opaque",
label: "Opaque",
title: "Opaque overlay",
description:
"Default dimmed backdrop without extra blur on the page behind the drawer.",
},
{
variant: "blur",
label: "Blur",
title: "Blurred overlay",
description:
"Backdrop blur so content behind the drawer is visibly softened.",
},
{
variant: "transparent",
label: "Transparent",
title: "Transparent overlay",
description:
"No dimmed backdrop \u2014 the page stays fully visible behind the drawer surface.",
},
] as const;
export function DrawerOverlayVariant() {
return (
<div className="flex flex-wrap gap-2">
{variants.map((v) => (
<Drawer key={v.variant}>
<DrawerTrigger asChild>
<Button variant="tertiary">{v.label}</Button>
</DrawerTrigger>
<DrawerContent overlayVariant={v.variant}>
<DrawerHeader>
<DrawerTitle>{v.title}</DrawerTitle>
<DrawerDescription>{v.description}</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose asChild>
<Button className="w-full">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
))}
</div>
);
}
Keep actions visible while the content scrolls.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerScrollableContent() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="tertiary">Scrollable Content</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Scrollable Content</DrawerTitle>
<DrawerDescription>
This is a drawer with scrollable content.
</DrawerDescription>
</DrawerHeader>
<div className="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
{Array.from({ length: 10 }).map((_, index) => (
<p key={index} className="mb-4 leading-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
))}
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button>Submit</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
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";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
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 DrawerResponsiveDialog() {
const [open, setOpen] = React.useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button variant="tertiary" />}>
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 asChild>
<Button variant="tertiary">Edit Profile</Button>
</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="px-4" />
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button className="w-full" variant="tertiary">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
function ProfileForm({ className }: React.ComponentProps<"form">) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
className={cn("grid items-start gap-6", className)}
>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
variant="secondary"
type="email"
id="email"
defaultValue="maqed@example.com"
/>
</div>
<div className="grid gap-3">
<Label htmlFor="username">Username</Label>
<Input variant="secondary" id="username" defaultValue="@0xMaqed" />
</div>
<Button type="submit">Save changes</Button>
</form>
);
}
To enable RTL support in herocn, see the RTL configuration guide.
"use client";
import {
type Translations,
useTranslation,
} from "@/components/language-selector";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Field, FieldGroup } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const translations: Translations = {
en: {
dir: "ltr",
values: {
openDrawer: "Open Drawer",
editProfile: "Edit profile",
description:
"Make changes to your profile here. Click save when you're done.",
name: "Name",
username: "Username",
cancel: "Cancel",
saveChanges: "Save changes",
},
},
ar: {
dir: "rtl",
values: {
openDrawer: "فتح الدرج",
editProfile: "تعديل الملف الشخصي",
description:
"قم بإجراء تغييرات على ملفك الشخصي هنا. انقر فوق حفظ عند الانتهاء.",
name: "الاسم",
username: "اسم المستخدم",
cancel: "إلغاء",
saveChanges: "حفظ التغييرات",
},
},
he: {
dir: "rtl",
values: {
openDrawer: "פתח מגירה",
editProfile: "ערוך פרופיל",
description: "בצע שינויים בפרופיל שלך כאן. לחץ על שמור כשתסיים.",
name: "שם",
username: "שם משתמש",
cancel: "בטל",
saveChanges: "שמור שינויים",
},
},
};
export function DrawerRtl() {
const { dir, language, t } = useTranslation(translations, "ar");
return (
<div lang={language} dir={dir}>
<Drawer>
<DrawerTrigger asChild>
<Button variant="tertiary">{t.openDrawer}</Button>
</DrawerTrigger>
<DrawerContent
dir={dir}
data-lang={dir === "rtl" ? language : undefined}
>
<DrawerHeader>
<DrawerTitle>{t.editProfile}</DrawerTitle>
<DrawerDescription>{t.description}</DrawerDescription>
</DrawerHeader>
<FieldGroup className="px-4">
<Field>
<Label htmlFor="drawer-rtl-name">{t.name}</Label>
<Input
variant="secondary"
id="drawer-rtl-name"
defaultValue="Maged Ibrahim"
/>
</Field>
<Field>
<Label htmlFor="drawer-rtl-username">{t.username}</Label>
<Input
variant="secondary"
id="drawer-rtl-username"
defaultValue="@0xMaqed"
/>
</Field>
</FieldGroup>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="tertiary">{t.cancel}</Button>
</DrawerClose>
<Button type="submit">{t.saveChanges}</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</div>
);
}
See the Vaul documentation for the full API reference.