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 variant="outline" size="sm">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 variant="outline" size="sm">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
open={firstOpen}
onOpenChange={(nextOpen) => {
setFirstOpen(nextOpen);
if (!nextOpen) {
setSecondOpen(false);
setThirdOpen(false);
}
}}
>
<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
open={secondOpen}
onOpenChange={(nextOpen) => {
setSecondOpen(nextOpen);
if (!nextOpen) {
setThirdOpen(false);
}
}}
>
<DrawerTrigger render={<Button variant="link" size="sm" className={styles.linkButton}>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 open={thirdOpen} onOpenChange={setThirdOpen}>
<DrawerTrigger render={<Button variant="link" className={styles.linkButton}>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 id="invite-email" className={styles.input} defaultValue="colleague@company.com" />
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="invite-role">Role</label>
<textarea id="invite-role" className={styles.textarea} defaultValue="Editor" rows={3} />
</div>
<div className={styles.actions}>
<DrawerClose render={<Button variant="outline" size="sm">Send Invite</Button>} />
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
</div>
<DrawerClose render={<Button variant="outline" size="sm">Close</Button>} />
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
</div>
<DrawerClose render={<Button variant="outline" size="sm">Close</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,
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 className={styles.cards} aria-hidden>
{Array.from({ length: 20 }, (_, index) => (
<div className={styles.card} key={index} />
))}
</div>
<div className={styles.actions}>
<DrawerClose render={<Button variant="outline" size="sm">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 variant="outline" size="sm">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 swipeDirection="right" modal={false} disablePointerDismissal>
<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 variant="outline" size="sm">Dismiss</Button>} />
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}
"use client";
import * as React from "react";
import Link from "next/link";
import { ScrollArea } from "@base-ui/react/scroll-area";
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 style={{ position: undefined }} className={styles.scrollAreaRoot}>
<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 width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M0.75 0.75L6 6M11.25 11.25L6 6M6 6L0.75 11.25M6 6L11.25 0.75"
stroke="currentcolor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</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 key={item.label} className={styles.item}>
<Link className={styles.link} href={item.href}>
{item.label}
</Link>
</li>
))}
</ul>
<ul className={styles.longList} aria-label="Long list">
{LONG_LIST.map((item) => (
<li key={item.label} className={styles.item}>
<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 open={open} onOpenChange={setOpen}>
<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 className={styles.actions} aria-label="File actions">
{ACTIONS.map((action, index) => (
<li key={action} className={styles.action}>
{index === 0 && (
<DrawerClose className={styles.visuallyHidden}>
Close file actions
</DrawerClose>
)}
<button
type="button"
className={styles.actionButton}
onClick={() => setOpen(false)}
>
{action}
</button>
</li>
))}
</ul>
</DrawerContent>
<div className={styles.dangerSurface}>
<button
type="button"
className={styles.dangerButton}
onClick={() => setOpen(false)}
>
Delete
</button>
</div>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
);
}