Drawer
A panel that slides in from the edge of the screen with swipe-to-dismiss gestures.
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-side.module.css";
export default function DrawerSide() {
return (
<Drawer swipeDirection="right">
<DrawerTrigger render={<Button>View Activity</Button>} />
<DrawerPortal>
<DrawerBackdrop />
<DrawerViewport className={styles.viewport}>
<DrawerPopup className={styles.popup}>
<DrawerContent>
<DrawerTitle>Activity</DrawerTitle>
<DrawerDescription>Recent changes across your workspace this week.</DrawerDescription>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Close
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
npx shadcn@latest add @roiui/drawernpx shadcn@latest add @roiui/drawer-tailwindanatomy
<Drawer>
<DrawerTrigger />
<DrawerPortal>
<DrawerBackdrop />
<DrawerViewport>
<DrawerPopup>
<DrawerHandle />
<DrawerContent>
<DrawerTitle />
<DrawerDescription />
<DrawerClose />
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-demo.module.css";
export default function DrawerDemo() {
return (
<Drawer>
<DrawerTrigger render={<Button>View Storage</Button>} />
<DrawerPortal>
<DrawerBackdrop />
<DrawerViewport>
<DrawerPopup className={styles.popup}>
<DrawerHandle />
<DrawerContent>
<DrawerTitle className={styles.title}>Storage</DrawerTitle>
<DrawerDescription className={styles.description}>
You've used 4.2 GB of 10 GB. Upgrade your plan for more space.
</DrawerDescription>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Done
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-nested.module.css";
export default function DrawerNested() {
const [firstOpen, setFirstOpen] = React.useState(false);
const [secondOpen, setSecondOpen] = React.useState(false);
const [thirdOpen, setThirdOpen] = React.useState(false);
return (
<Drawer
onOpenChange={(nextOpen: boolean) => {
setFirstOpen(nextOpen);
if (!nextOpen) {
setSecondOpen(false);
setThirdOpen(false);
}
}}
open={firstOpen}
>
<DrawerTrigger render={<Button>Workspace Settings</Button>} />
<DrawerPortal>
<DrawerBackdrop />
<DrawerViewport>
<DrawerPopup className={styles.popup}>
<DrawerHandle className={styles.handle} />
<DrawerContent className={styles.content}>
<DrawerTitle className={styles.title}>Workspace</DrawerTitle>
<DrawerDescription className={styles.description}>
Manage your workspace settings, members, and billing.
</DrawerDescription>
<div className={styles.actions}>
<div className={styles.actionsLeft}>
<Drawer
onOpenChange={(nextOpen: boolean) => {
setSecondOpen(nextOpen);
if (!nextOpen) {
setThirdOpen(false);
}
}}
open={secondOpen}
>
<DrawerTrigger
render={
<Button className={styles.linkButton} size="sm" variant="link">
Team members
</Button>
}
/>
<DrawerPortal>
<DrawerViewport>
<DrawerPopup className={styles.popup}>
<DrawerHandle className={styles.handle} />
<DrawerContent className={styles.content}>
<DrawerTitle className={styles.title}>Team</DrawerTitle>
<DrawerDescription className={styles.description}>
Manage who has access to this workspace.
</DrawerDescription>
<ul className={styles.list}>
<li>5 active members</li>
<li>2 pending invitations</li>
<li>3 roles configured</li>
</ul>
<div className={styles.actions}>
<div className={styles.actionsLeft}>
<Drawer onOpenChange={setThirdOpen} open={thirdOpen}>
<DrawerTrigger
render={
<Button className={styles.linkButton} variant="link">
Invite member
</Button>
}
/>
<DrawerPortal>
<DrawerViewport>
<DrawerPopup className={styles.popup}>
<DrawerHandle className={styles.handle} />
<DrawerContent className={styles.content}>
<DrawerTitle className={styles.title}>Invite Member</DrawerTitle>
<DrawerDescription className={styles.description}>
Send an invitation to join your workspace.
</DrawerDescription>
<div className={styles.field}>
<label className={styles.label} htmlFor="invite-email">
Email address
</label>
<input
className={styles.input}
defaultValue="colleague@company.com"
id="invite-email"
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="invite-role">
Role
</label>
<textarea
className={styles.textarea}
defaultValue="Editor"
id="invite-role"
rows={3}
/>
</div>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Send Invite
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
</div>
<DrawerClose
render={
<Button size="sm" variant="outline">
Close
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
</div>
<DrawerClose
render={
<Button size="sm" variant="outline">
Close
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import type * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-snap-points.module.css";
const TOP_MARGIN_REM = 1;
const VISIBLE_SNAP_POINTS_REM = [30];
function toViewportSnapPoint(heightRem: number) {
return `${heightRem + TOP_MARGIN_REM}rem`;
}
const snapPoints = [...VISIBLE_SNAP_POINTS_REM.map(toViewportSnapPoint), 1];
export default function DrawerSnapPoints() {
return (
<Drawer snapPoints={snapPoints}>
<DrawerTrigger render={<Button>Explore</Button>} />
<DrawerPortal>
<DrawerBackdrop />
<DrawerViewport className={styles.viewport}>
<DrawerPopup
className={styles.popup}
style={{ "--top-margin": `${TOP_MARGIN_REM}rem` } as React.CSSProperties}
>
<div className={styles.dragArea}>
<div className={styles.handle} />
<DrawerTitle className={styles.title}>Discover</DrawerTitle>
</div>
<DrawerContent className={styles.scroll}>
<div className={styles.content}>
<DrawerDescription className={styles.description}>
Trending topics and curated picks based on your interests.
</DrawerDescription>
<div aria-hidden className={styles.cards}>
{Array.from({ length: 20 }, (_, index) => (
<div className={styles.card} key={index} />
))}
</div>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Close
</Button>
}
/>
</div>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerIndent,
DrawerIndentBackground,
DrawerPopup,
DrawerPortal,
DrawerProvider,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-indent.module.css";
export default function DrawerIndentDemo() {
const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null);
return (
<DrawerProvider>
<div className={styles.root} ref={setPortalContainer}>
<DrawerIndentBackground className={styles.indentBackground} />
<DrawerIndent className={styles.indent}>
<div className={styles.center}>
<DrawerRoot modal={false}>
<DrawerTrigger render={<Button>New Note</Button>} />
<DrawerPortal container={portalContainer}>
<DrawerBackdrop className={styles.backdrop} />
<DrawerViewport className={styles.viewport}>
<DrawerPopup>
<DrawerHandle />
<DrawerContent>
<DrawerTitle className={styles.title}>Quick Note</DrawerTitle>
<DrawerDescription className={styles.description}>
Capture a thought before it slips away.
</DrawerDescription>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Done
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</DrawerRoot>
</div>
</DrawerIndent>
</div>
</DrawerProvider>
);
}
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-non-modal.module.css";
export default function DrawerNonModal() {
return (
<Drawer disablePointerDismissal modal={false} swipeDirection="right">
<DrawerTrigger render={<Button>Start Export</Button>} />
<DrawerPortal>
<DrawerViewport className={styles.viewport}>
<DrawerPopup className={styles.popup}>
<DrawerContent>
<DrawerTitle>Export Complete</DrawerTitle>
<DrawerDescription>
Your file has been saved to the Downloads folder. You can continue working while it processes.
</DrawerDescription>
<div className={styles.actions}>
<DrawerClose
render={
<Button size="sm" variant="outline">
Dismiss
</Button>
}
/>
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import { ScrollArea } from "@base-ui/react/scroll-area";
import Link from "next/link";
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-mobile-nav.module.css";
const ITEMS = [
{ href: "#", label: "Home" },
{ href: "#", label: "Products" },
{ href: "#", label: "Pricing" },
{ href: "#", label: "About" },
] as const;
const LONG_LIST = Array.from({ length: 50 }, (_, i) => ({
href: "#",
label: `Page ${i + 1}`,
}));
export default function DrawerMobileNav() {
return (
<Drawer>
<DrawerTrigger render={<Button>Open Menu</Button>} />
<DrawerPortal>
<DrawerBackdrop className={styles.backdrop} />
<DrawerViewport className={styles.viewport}>
<ScrollArea.Root className={styles.scrollAreaRoot} style={{ position: undefined }}>
<ScrollArea.Viewport className={styles.scrollAreaViewport}>
<ScrollArea.Content className={styles.scrollContent}>
<DrawerPopup className={styles.popup}>
<nav aria-label="Navigation" className={styles.panel}>
<div className={styles.header}>
<div aria-hidden className={styles.headerSpacer} />
<DrawerHandle className={styles.handle} />
<DrawerClose aria-label="Close menu" className={styles.closeButton}>
<svg fill="none" height="12" viewBox="0 0 12 12" width="12">
<path
d="M0.75 0.75L6 6M11.25 11.25L6 6M6 6L0.75 11.25M6 6L11.25 0.75"
stroke="currentcolor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
</DrawerClose>
</div>
<DrawerContent className={styles.content}>
<DrawerTitle className={styles.title}>Navigation</DrawerTitle>
<DrawerDescription className={styles.description}>
Browse all sections. Pull down to dismiss.
</DrawerDescription>
<div className={styles.scrollArea}>
<ul className={styles.list}>
{ITEMS.map((item) => (
<li className={styles.item} key={item.label}>
<Link className={styles.link} href={item.href}>
{item.label}
</Link>
</li>
))}
</ul>
<ul aria-label="Long list" className={styles.longList}>
{LONG_LIST.map((item) => (
<li className={styles.item} key={item.label}>
<Link className={styles.link} href={item.href}>
{item.label}
</Link>
</li>
))}
</ul>
</div>
</DrawerContent>
</nav>
</DrawerPopup>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.scrollbar}>
<ScrollArea.Thumb className={styles.scrollbarThumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
Drawer,
DrawerBackdrop,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-action-sheet.module.css";
const ACTIONS = ["Share", "Duplicate", "Move to Folder", "Pin to Top", "Print"];
export default function DrawerActionSheet() {
const [open, setOpen] = React.useState(false);
return (
<Drawer onOpenChange={setOpen} open={open}>
<DrawerTrigger render={<Button>More Actions</Button>} />
<DrawerPortal>
<DrawerBackdrop className={styles.backdrop} />
<DrawerViewport>
<DrawerPopup className={styles.popup}>
<DrawerContent className={styles.surface}>
<DrawerTitle className={styles.visuallyHidden}>File actions</DrawerTitle>
<DrawerDescription className={styles.visuallyHidden}>Choose an action for this file.</DrawerDescription>
<ul aria-label="File actions" className={styles.actions}>
{ACTIONS.map((action, index) => (
<li className={styles.action} key={action}>
{index === 0 && <DrawerClose className={styles.visuallyHidden}>Close file actions</DrawerClose>}
<button className={styles.actionButton} onClick={() => setOpen(false)} type="button">
{action}
</button>
</li>
))}
</ul>
</DrawerContent>
<div className={styles.dangerSurface}>
<button className={styles.dangerButton} onClick={() => setOpen(false)} type="button">
Delete
</button>
</div>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}