Github

Drawer

A drawer component that slides in from any side of the screen, built on top of Vaul.

"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&apos;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>
	);
}

Installation

pnpm dlx shadcn@latest add @herocn/drawer

Usage

import {
  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>

Composition

Use the following composition to build a Drawer:

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

Examples

Direction

Use 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>
	);
}

Overlay variant

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>
	);
}

Scrollable Content

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>
	);
}

Responsive Dialog

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&apos;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&apos;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>
	);
}

RTL

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>
	);
}

API Reference

See the Vaul documentation for the full API reference.