Drawer
A panel that slides in from the edge of the screen with swipe gestures, snap points, and nested drawer support. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
return (
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Open drawer
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader className="text-center">
<DrawerTitle>Notifications</DrawerTitle>
<DrawerDescription>
This is the description of the drawer.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/drawer
Usage
import {
Drawer,
DrawerClose,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer>
<DrawerTrigger>Open Drawer</DrawerTrigger>
<DrawerPopup>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
<DrawerDescription>Drawer Description</DrawerDescription>
</DrawerHeader>
<DrawerPanel>Content</DrawerPanel>
<DrawerFooter>
<DrawerClose>Close</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>Position
Use the position prop on Drawer to control which edge the drawer slides in from. Defaults to "bottom".
<Drawer position="right">...</Drawer>
<Drawer position="left">...</Drawer>
<Drawer position="top">...</Drawer>
<Drawer position="bottom">...</Drawer>Variants
The DrawerPopup component supports a variant prop to control its shape:
default(default): Rounded corners facing inwardinset: Adds screen-edge inset spacing on larger viewportsstraight: No rounded corners — flush to the screen edge
<DrawerPopup variant="inset">...</DrawerPopup>
<DrawerPopup variant="straight">...</DrawerPopup>Footer Variant
The DrawerFooter component supports a variant prop:
default: Includes a border-top, background color, and paddingbare: Removes the border and background for a minimal appearance
<DrawerFooter variant="bare">...</DrawerFooter>Drag bar
Pass showBar to DrawerPopup to render a visible drag handle bar.
<DrawerPopup showBar>...</DrawerPopup>Examples
Multiple Positions — Inset
Opens a drawer from each of the four edges (top, right, bottom, left) using the inset variant with screen-edge spacing.
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
return (
<div className="flex flex-wrap gap-2">
<Drawer position="right">
<DrawerTrigger render={<Button variant="outline" />}>
Right
</DrawerTrigger>
<DrawerPopup variant="inset">
<DrawerHeader>
<DrawerTitle>Right</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the right.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer position="left">
<DrawerTrigger render={<Button variant="outline" />}>
Left
</DrawerTrigger>
<DrawerPopup variant="inset">
<DrawerHeader>
<DrawerTitle>Left</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the left.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer position="top">
<DrawerTrigger render={<Button variant="outline" />}>Top</DrawerTrigger>
<DrawerPopup variant="inset">
<DrawerHeader>
<DrawerTitle>Top</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the top.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Bottom
</DrawerTrigger>
<DrawerPopup variant="inset">
<DrawerHeader>
<DrawerTitle>Bottom</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the bottom.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
</div>
);
}
Multiple Positions — Straight
Same four-edge positioning demo using the straight variant for flush, full-bleed drawers with no rounded corners.
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
return (
<div className="flex flex-wrap gap-2">
<Drawer position="right">
<DrawerTrigger render={<Button variant="outline" />}>
Right
</DrawerTrigger>
<DrawerPopup variant="straight">
<DrawerHeader>
<DrawerTitle>Right</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the right.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer position="left">
<DrawerTrigger render={<Button variant="outline" />}>
Left
</DrawerTrigger>
<DrawerPopup variant="straight">
<DrawerHeader>
<DrawerTitle>Left</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the left.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer position="top">
<DrawerTrigger render={<Button variant="outline" />}>Top</DrawerTrigger>
<DrawerPopup variant="straight">
<DrawerHeader>
<DrawerTitle>Top</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the top.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Bottom
</DrawerTrigger>
<DrawerPopup variant="straight">
<DrawerHeader>
<DrawerTitle>Bottom</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<p className="text-muted-foreground text-sm">
Content from the bottom.
</p>
</DrawerPanel>
</DrawerPopup>
</Drawer>
</div>
);
}
Multi-step Nested Drawers
Chains multiple drawers as a step-by-step wizard flow, each opening on top of the previous one.
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
return (
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Nested drawers
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader className="text-center">
<DrawerTitle>First step</DrawerTitle>
<DrawerDescription>
This is the first step. Tap the button below to continue to the next
screen.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Continue
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader className="text-center">
<DrawerTitle>Second step</DrawerTitle>
<DrawerDescription>
You've reached the second step. Tap the button below to
continue to the next screen.
</DrawerDescription>
</DrawerHeader>
<DrawerPanel>
<div className="flex justify-center">
<div className="size-48 shrink-0 rounded-xl border bg-muted" />
</div>
</DrawerPanel>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="ghost" />}>
Back
</DrawerClose>
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
Continue
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader className="text-center">
<DrawerTitle>Third step</DrawerTitle>
<DrawerDescription>
You've reached the final step. You can close this
drawer or go back.
</DrawerDescription>
</DrawerHeader>
<DrawerPanel>
<div className="flex justify-center">
<div className="size-32 shrink-0 rounded-full border bg-muted" />
</div>
</DrawerPanel>
</DrawerPopup>
</Drawer>
</DrawerFooter>
</DrawerPopup>
</Drawer>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Navigation Menu
A side drawer containing a full navigation menu, the standard pattern for mobile app side navigation.
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
return (
<Drawer position="left">
<DrawerTrigger render={<Button variant="outline" />}>
Open menu
</DrawerTrigger>
<DrawerPopup showCloseButton variant="straight">
<DrawerHeader>
<DrawerTitle>Menu</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<nav className="-mx-[calc(--spacing(3)-1px)] flex flex-col gap-0.5">
<DrawerClose
nativeButton={false}
render={
<Button
className="justify-start"
render={<Link href="#" />}
variant="ghost"
/>
}
>
Home
</DrawerClose>
<DrawerClose
nativeButton={false}
render={
<Button
className="justify-start"
render={<Link href="#" />}
variant="ghost"
/>
}
>
Profile
</DrawerClose>
<DrawerClose
nativeButton={false}
render={
<Button
className="justify-start"
render={<Link href="#" />}
variant="ghost"
/>
}
>
Settings
</DrawerClose>
<DrawerClose
nativeButton={false}
render={
<Button
className="justify-start"
render={<Link href="#" />}
variant="ghost"
/>
}
>
Sign out
</DrawerClose>
</nav>
</DrawerPanel>
</DrawerPopup>
</Drawer>
);
}
Filter & Sort Panel
A bottom or side drawer with filter checkboxes and sort controls for refining list or grid views.
import { SlidersHorizontalIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerMenu,
DrawerMenuCheckboxItem,
DrawerMenuGroup,
DrawerMenuGroupLabel,
DrawerMenuRadioGroup,
DrawerMenuRadioItem,
DrawerMenuSeparator,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Component() {
return (
<Drawer position="right">
<DrawerTrigger render={<Button variant="outline" />}>
<SlidersHorizontalIcon className="size-4" />
Filters
</DrawerTrigger>
<DrawerPopup showCloseButton variant="straight">
<DrawerHeader>
<DrawerTitle>Filter & Sort</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<DrawerMenu>
<DrawerMenuGroup>
<DrawerMenuGroupLabel>Category</DrawerMenuGroupLabel>
<DrawerMenuCheckboxItem defaultChecked>
Design
</DrawerMenuCheckboxItem>
<DrawerMenuCheckboxItem defaultChecked>
Engineering
</DrawerMenuCheckboxItem>
<DrawerMenuCheckboxItem>Marketing</DrawerMenuCheckboxItem>
<DrawerMenuCheckboxItem>Product</DrawerMenuCheckboxItem>
</DrawerMenuGroup>
<DrawerMenuSeparator />
<DrawerMenuGroup>
<DrawerMenuGroupLabel>Status</DrawerMenuGroupLabel>
<DrawerMenuCheckboxItem defaultChecked>
Active
</DrawerMenuCheckboxItem>
<DrawerMenuCheckboxItem>Archived</DrawerMenuCheckboxItem>
<DrawerMenuCheckboxItem>Draft</DrawerMenuCheckboxItem>
</DrawerMenuGroup>
<DrawerMenuSeparator />
<DrawerMenuGroup>
<DrawerMenuGroupLabel>Sort by</DrawerMenuGroupLabel>
<DrawerMenuRadioGroup defaultValue="newest">
<DrawerMenuRadioItem value="newest">Newest</DrawerMenuRadioItem>
<DrawerMenuRadioItem value="oldest">Oldest</DrawerMenuRadioItem>
<DrawerMenuRadioItem value="name">Name A–Z</DrawerMenuRadioItem>
<DrawerMenuRadioItem value="popular">
Most popular
</DrawerMenuRadioItem>
</DrawerMenuRadioGroup>
</DrawerMenuGroup>
</DrawerMenu>
</DrawerPanel>
<DrawerFooter>
<DrawerClose render={<Button variant="ghost" />}>Reset</DrawerClose>
<DrawerClose render={<Button />}>Apply filters</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Shopping Cart
A right-side drawer showing cart items, quantities, subtotal, and a checkout button.
import { MinusIcon, PlusIcon, ShoppingCartIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const cartItems = [
{ id: 1, name: "Wireless Headphones", price: 79.99, qty: 1 },
{ id: 2, name: "Mechanical Keyboard", price: 129.0, qty: 1 },
{ id: 3, name: "USB-C Hub", price: 49.99, qty: 2 },
];
export default function Component() {
const subtotal = cartItems.reduce(
(sum, item) => sum + item.price * item.qty,
0,
);
return (
<Drawer position="right">
<DrawerTrigger render={<Button variant="outline" />}>
<ShoppingCartIcon className="size-4" />
Cart ({cartItems.length})
</DrawerTrigger>
<DrawerPopup showCloseButton variant="straight">
<DrawerHeader>
<DrawerTitle>Your cart</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<ul className="divide-y divide-border">
{cartItems.map((item) => (
<li className="flex items-center gap-3 py-3" key={item.id}>
<div className="size-12 shrink-0 rounded-lg bg-muted" />
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-sm">{item.name}</p>
<p className="text-muted-foreground text-sm">
${item.price.toFixed(2)}
</p>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button className="size-6" size="icon" variant="outline">
<MinusIcon className="size-3" />
</Button>
<span className="w-6 text-center text-sm tabular-nums">
{item.qty}
</span>
<Button className="size-6" size="icon" variant="outline">
<PlusIcon className="size-3" />
</Button>
</div>
</li>
))}
</ul>
</DrawerPanel>
<DrawerFooter className="flex md:flex-col">
<div className="mb-1 flex w-full items-center justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-semibold">${subtotal.toFixed(2)}</span>
</div>
<DrawerClose render={<Button className="w-full" />}>
Checkout
</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Notification Center
A drawer listing recent notifications with timestamps and mark-as-read actions.
import { BellIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const notifications = [
{
avatar: "https://github.com/shadcn.png",
id: 1,
initials: "MW",
message: "Commented on your pull request: 'looks good to me!'",
name: "Margaret Welsh",
time: "2m ago",
unread: true,
},
{
avatar: "",
id: 2,
initials: "BB",
message: "Assigned you to the task 'Update onboarding flow'.",
name: "Bora Baloglu",
time: "18m ago",
unread: true,
},
{
avatar: "",
id: 3,
initials: "SR",
message: "Mentioned you in a comment: '@you can you take a look?'",
time: "1h ago",
unread: false,
},
{
avatar: "",
id: 4,
initials: "KN",
message: "Approved your design review submission.",
name: "Kai Nakamura",
time: "3h ago",
unread: false,
},
];
export default function Component() {
return (
<Drawer>
<DrawerTrigger render={<Button size="icon" variant="outline" />}>
<BellIcon className="size-4" />
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader className="flex-row items-center justify-between">
<DrawerTitle>Notifications</DrawerTitle>
<Button className="h-auto py-1 text-xs" variant="ghost">
Mark all as read
</Button>
</DrawerHeader>
<DrawerPanel>
<ul className="divide-y divide-border">
{notifications.map((n) => (
<li className="flex items-start gap-3 py-3" key={n.id}>
<Avatar className="mt-0.5 size-8 shrink-0">
{n.avatar && <AvatarImage alt={n.name} src={n.avatar} />}
<AvatarFallback>{n.initials}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="font-medium text-sm leading-none">{n.name}</p>
<p className="mt-0.5 line-clamp-2 text-muted-foreground text-sm">
{n.message}
</p>
<p className="mt-1 text-muted-foreground text-xs">{n.time}</p>
</div>
{n.unread && (
<span className="mt-1.5 size-2 shrink-0 rounded-full bg-primary" />
)}
</li>
))}
</ul>
</DrawerPanel>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
User Preferences & Theme Settings
A right-side straight drawer with theme switcher tiles (Light / Dark / System), a language select, and a push-notification toggle switch.
"use client";
import {
BellIcon,
GlobeIcon,
MoonIcon,
SettingsIcon,
SunIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function Particle() {
const [theme, setTheme] = useState<"light" | "dark" | "system">("system");
const [language, setLanguage] = useState("English");
const [notifs, setNotifs] = useState(true);
return (
<Drawer position="right">
<DrawerTrigger render={<Button size="icon" variant="outline" />}>
<SettingsIcon className="size-4" />
</DrawerTrigger>
<DrawerPopup showCloseButton variant="straight">
<DrawerHeader>
<DrawerTitle>Preferences</DrawerTitle>
</DrawerHeader>
<DrawerPanel className="space-y-6">
<div className="space-y-2">
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Appearance
</p>
<div className="grid grid-cols-3 gap-2">
{(["light", "dark", "system"] as const).map((t) => (
<button
className={`flex flex-col items-center gap-1.5 rounded-lg border p-3 font-medium text-xs transition-colors ${
theme === t
? "border-primary bg-primary/5 text-primary"
: "text-muted-foreground hover:border-foreground/20"
}`}
key={t}
onClick={() => setTheme(t)}
type="button"
>
{t === "light" ? (
<SunIcon className="size-4" />
) : t === "dark" ? (
<MoonIcon className="size-4" />
) : (
<span className="text-base">⚙</span>
)}
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
</div>
<div className="space-y-2">
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Language
</p>
<div className="flex items-center gap-2 rounded-lg border px-3 py-2.5">
<GlobeIcon className="size-4 shrink-0 text-muted-foreground" />
<select
className="flex-1 bg-transparent text-sm outline-none"
onChange={(e) => setLanguage(e.target.value)}
value={language}
>
{["English", "French", "German", "Spanish", "Japanese"].map(
(l) => (
<option key={l} value={l}>
{l}
</option>
),
)}
</select>
</div>
</div>
<div className="space-y-2">
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Notifications
</p>
<div className="flex items-center justify-between rounded-lg border px-3 py-2.5">
<div className="flex items-center gap-2">
<BellIcon className="size-4 text-muted-foreground" />
<span className="text-sm">Push notifications</span>
</div>
<button
className={`relative h-5 w-9 rounded-full transition-colors ${notifs ? "bg-primary" : "bg-muted-foreground/30"}`}
onClick={() => setNotifs((v) => !v)}
type="button"
>
<span
className={`absolute top-0.5 size-4 rounded-full bg-white shadow transition-transform ${notifs ? "translate-x-4" : "translate-x-0.5"}`}
/>
</button>
</div>
</div>
</DrawerPanel>
<DrawerFooter>
<DrawerClose render={<Button className="w-full" variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
File Attachments with Upload
A bottom drawer with a drag-and-drop upload zone and an existing attachments list — individual files can be removed before confirming the selection.
"use client";
import {
FileTextIcon,
ImageIcon,
PaperclipIcon,
TrashIcon,
UploadIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
type MockFile = {
id: number;
name: string;
size: string;
type: "image" | "doc";
};
const INITIAL_FILES: MockFile[] = [
{ id: 1, name: "Q3-report.pdf", size: "2.4 MB", type: "doc" },
{ id: 2, name: "hero-banner.png", size: "840 KB", type: "image" },
{ id: 3, name: "notes.txt", size: "12 KB", type: "doc" },
];
export default function Particle() {
const [files, setFiles] = useState<MockFile[]>(INITIAL_FILES);
const remove = (id: number) =>
setFiles((prev) => prev.filter((f) => f.id !== id));
return (
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
<PaperclipIcon className="size-4" />
Attachments ({files.length})
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader>
<DrawerTitle>Attachments</DrawerTitle>
</DrawerHeader>
<DrawerPanel className="space-y-3">
<label className="flex cursor-pointer flex-col items-center gap-2 rounded-xl border-2 border-dashed p-6 text-center transition-colors hover:border-primary hover:bg-primary/5">
<UploadIcon className="size-6 text-muted-foreground" />
<span className="font-medium text-sm">
Drop files or click to upload
</span>
<span className="text-muted-foreground text-xs">
PNG, JPG, PDF up to 10 MB
</span>
<input
accept=".png,.jpg,.jpeg,.pdf,.txt"
className="sr-only"
type="file"
/>
</label>
{files.length > 0 && (
<ul className="divide-y divide-border rounded-lg border">
{files.map((file) => (
<li
className="flex items-center gap-3 px-3 py-2.5"
key={file.id}
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
{file.type === "image" ? (
<ImageIcon className="size-4 text-muted-foreground" />
) : (
<FileTextIcon className="size-4 text-muted-foreground" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-sm">{file.name}</p>
<p className="text-muted-foreground text-xs">{file.size}</p>
</div>
<button
className="shrink-0 rounded-md p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
onClick={() => remove(file.id)}
type="button"
>
<TrashIcon className="size-4" />
</button>
</li>
))}
</ul>
)}
</DrawerPanel>
<DrawerFooter>
<DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
<DrawerClose render={<Button />}>
Attach {files.length} file{files.length !== 1 ? "s" : ""}
</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Activity Log with Status Timeline
A right-side straight drawer rendering a vertical timeline of deployment and build events, each tagged with Completed, In progress, or Pending status badges.
"use client";
import {
ActivityIcon,
CheckCircle2Icon,
CircleIcon,
ClockIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
type Status = "completed" | "in_progress" | "pending";
const EVENTS: {
id: number;
action: string;
user: string;
time: string;
status: Status;
}[] = [
{
action: "Deployment to production",
id: 1,
status: "completed",
time: "2 min ago",
user: "Margaret Welsh",
},
{
action: "Running test suite",
id: 2,
status: "in_progress",
time: "5 min ago",
user: "System",
},
{
action: "PR #142 merged",
id: 3,
status: "completed",
time: "18 min ago",
user: "Bora Baloglu",
},
{
action: "Build triggered",
id: 4,
status: "completed",
time: "20 min ago",
user: "System",
},
{
action: "Staging deploy scheduled",
id: 5,
status: "pending",
time: "1h ago",
user: "Sofia Reyes",
},
{
action: "Database migration",
id: 6,
status: "completed",
time: "2h ago",
user: "System",
},
{
action: "Cache invalidation",
id: 7,
status: "completed",
time: "3h ago",
user: "System",
},
];
const STATUS_CONFIG: Record<
Status,
{ icon: typeof CheckCircle2Icon; color: string; label: string }
> = {
completed: {
color: "text-green-500",
icon: CheckCircle2Icon,
label: "Completed",
},
in_progress: {
color: "text-amber-500",
icon: ClockIcon,
label: "In progress",
},
pending: {
color: "text-muted-foreground",
icon: CircleIcon,
label: "Pending",
},
};
export default function Particle() {
return (
<Drawer position="right">
<DrawerTrigger render={<Button variant="outline" />}>
<ActivityIcon className="size-4" />
Activity log
</DrawerTrigger>
<DrawerPopup showCloseButton variant="straight">
<DrawerHeader>
<DrawerTitle>Activity log</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<ol className="relative ml-3 space-y-4 border-border border-l">
{EVENTS.map((event) => {
const { icon: Icon, color, label } = STATUS_CONFIG[event.status];
return (
<li className="ml-5" key={event.id}>
<span
className={
"absolute -left-1.5 flex size-3 items-center justify-center rounded-full bg-background"
}
>
<Icon className={`size-3.5 ${color}`} />
</span>
<div className="space-y-0.5">
<p className="font-medium text-sm">{event.action}</p>
<div className="flex items-center gap-2 text-muted-foreground text-xs">
<span>{event.user}</span>
<span>·</span>
<span>{event.time}</span>
<span
className={`ml-auto rounded-full border px-1.5 py-0.5 font-medium text-[10px] ${
event.status === "completed"
? "border-green-200 text-green-600 dark:border-green-800 dark:text-green-400"
: event.status === "in_progress"
? "border-amber-200 text-amber-600 dark:border-amber-800 dark:text-amber-400"
: "border-border text-muted-foreground"
}`}
>
{label}
</span>
</div>
</div>
</li>
);
})}
</ol>
</DrawerPanel>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Help Center with FAQ Search
A bottom drawer with a live-filtered FAQ accordion — typing in the search field narrows the list and individual questions expand inline.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import {
ChevronDownIcon,
ChevronUpIcon,
HelpCircleIcon,
SearchIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
const FAQ = [
{
a: 'Go to Settings → Security and click "Change password". A reset link will be sent to your email.',
q: "How do I reset my password?",
},
{
a: "Yes. Navigate to Settings → Data & Privacy → Export data. Exports are delivered within 24 hours.",
q: "Can I export my data?",
},
{
a: 'Open your workspace, click "Members" in the sidebar, then "Invite" and enter their email addresses.',
q: "How do I invite team members?",
},
{
a: "Yes, our mobile app is available on iOS and Android. Search for the app in your device's app store.",
q: "Is there a mobile app?",
},
{
a: "Billing is per active seat, prorated to the day. Seats added mid-cycle appear on the next invoice.",
q: "How is billing calculated?",
},
];
export default function Particle() {
const [query, setQuery] = useState("");
const [expanded, setExpanded] = useState<number | null>(null);
const filtered = FAQ.filter(
(item) =>
item.q.toLowerCase().includes(query.toLowerCase()) ||
item.a.toLowerCase().includes(query.toLowerCase()),
);
return (
<Drawer>
<DrawerTrigger render={<Button variant="outline" />}>
<HelpCircleIcon className="size-4" />
Help & Support
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader>
<DrawerTitle>Help Center</DrawerTitle>
</DrawerHeader>
<DrawerPanel className="space-y-4">
<div className="relative">
<SearchIcon className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-9"
onChange={(e) => {
setQuery(e.target.value);
setExpanded(null);
}}
placeholder="Search help articles..."
value={query}
/>
</div>
<div className="space-y-1">
{filtered.length === 0 ? (
<p className="py-8 text-center text-muted-foreground text-sm">
No results for "{query}"
</p>
) : (
filtered.map((item, i) => (
<div className="rounded-lg border" key={i}>
<button
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left font-medium text-sm"
onClick={() => setExpanded(expanded === i ? null : i)}
type="button"
>
{item.q}
{expanded === i ? (
<ChevronUpIcon className="size-4 shrink-0 text-muted-foreground" />
) : (
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground" />
)}
</button>
{expanded === i && (
<p className="border-t px-4 py-3 text-muted-foreground text-sm">
{item.a}
</p>
)}
</div>
))
)}
</div>
</DrawerPanel>
<DrawerFooter>
<DrawerClose render={<Button variant="ghost" />}>Close</DrawerClose>
<Button>Contact support</Button>
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
Quick Actions Grid
A bottom drawer triggered by a contextual ellipsis button that presents an icon grid of actions (Edit, Share, Bookmark, Archive, Delete, etc.), each closing the drawer and reporting the chosen action.
"use client";
import {
ArchiveIcon,
BookmarkIcon,
DownloadIcon,
FlagIcon,
MessageSquareIcon,
MoreHorizontalIcon,
PencilIcon,
Share2Icon,
TrashIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const ACTIONS = [
{ description: "Modify this item", icon: PencilIcon, label: "Edit" },
{ description: "Add a comment", icon: MessageSquareIcon, label: "Comment" },
{ description: "Share with others", icon: Share2Icon, label: "Share" },
{ description: "Save for later", icon: BookmarkIcon, label: "Bookmark" },
{ description: "Export as file", icon: DownloadIcon, label: "Download" },
{ description: "Move to archive", icon: ArchiveIcon, label: "Archive" },
{ description: "Flag for review", icon: FlagIcon, label: "Report" },
{
description: "Remove permanently",
destructive: true,
icon: TrashIcon,
label: "Delete",
},
];
export default function Particle() {
const [lastAction, setLastAction] = useState<string | null>(null);
return (
<div className="flex items-center gap-3">
{lastAction && (
<p className="text-muted-foreground text-sm">
Action:{" "}
<span className="font-medium text-foreground">{lastAction}</span>
</p>
)}
<Drawer>
<DrawerTrigger render={<Button size="icon" variant="outline" />}>
<MoreHorizontalIcon className="size-4" />
</DrawerTrigger>
<DrawerPopup showBar>
<DrawerHeader>
<DrawerTitle>Actions</DrawerTitle>
</DrawerHeader>
<DrawerPanel>
<div className="grid grid-cols-4 gap-2">
{ACTIONS.map((action) => (
<DrawerClose
key={action.label}
render={
<button
className={`flex flex-col items-center gap-2 rounded-xl border p-3 text-center transition-colors ${
action.destructive
? "border-destructive/20 text-destructive hover:bg-destructive/5"
: "text-muted-foreground hover:border-foreground/20 hover:text-foreground"
}`}
onClick={() => setLastAction(action.label)}
type="button"
/>
}
>
<action.icon className="size-5" />
<span className="font-medium text-xs leading-none">
{action.label}
</span>
</DrawerClose>
))}
</div>
</DrawerPanel>
<DrawerFooter
className="justify-center sm:justify-center"
variant="bare"
>
<DrawerClose render={<Button variant="outline" />}>
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerPopup>
</Drawer>
</div>
);
}
On This Page

