import { MinusIcon, PlusIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export default function DrawerBasic() {
const [goal, setGoal] = React.useState(5);
return (
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>Set a goal</DrawerTrigger>
<DrawerContent>
<div className="mx-auto flex w-full max-w-sm flex-col">
<DrawerHeader>
<DrawerTitle>Daily focus goal</DrawerTitle>
<DrawerDescription>How many tasks do you want to finish each day?</DrawerDescription>
</DrawerHeader>
<div className="flex items-center justify-center gap-6 p-6">
<Button
variant="secondary"
size="icon-lg"
className="rounded-full"
aria-label="Decrease"
disabled={goal <= 1}
focusableWhenDisabled
onClick={() => setGoal((goal) => goal - 1)}
>
<MinusIcon />
</Button>
<div className="flex w-24 flex-col items-center">
<span className="text-5xl font-medium tabular-nums">{goal}</span>
<span className="text-2xs text-faint">tasks per day</span>
</div>
<Button
variant="secondary"
size="icon-lg"
className="rounded-full"
aria-label="Increase"
disabled={goal >= 20}
focusableWhenDisabled
onClick={() => setGoal((goal) => goal + 1)}
>
<PlusIcon />
</Button>
</div>
<DrawerFooter>
<DrawerClose render={<Button variant="primary" />}>Set goal</DrawerClose>
<DrawerClose render={<Button variant="secondary" />}>Cancel</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add drawer
pnpm dlx shadcn@latest add @jarvis/drawer
Usage
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function ShareDrawer() {
return (
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>Share</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Share project</DrawerTitle>
<DrawerDescription>Invite people or copy a link.</DrawerDescription>
</DrawerHeader>
<div className="p-4">…</div>
<DrawerFooter>
<DrawerClose render={<Button variant="secondary" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}A drawer slides up from the bottom and can be swiped away, which suits phones. For a panel without swiping, use a Sheet.
showSwipeHandle adds a grip. Keep a close button too, and always include a DrawerTitle.
Composition
Examples
Swipe direction
swipeDirection sets which way the drawer is swiped to close: down, up, left or right.
import { BellIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const notifications = [
{ title: "Sam Rivera mentioned you", detail: "in Update pricing page", time: "2m" },
{ title: "Invoice INV-1042 was paid", detail: "Acme Corp", time: "1h" },
{ title: "Deployment ready", detail: "website-redesign", time: "3h" },
];
export default function DrawerSwipeDirection() {
return (
<Drawer swipeDirection="right">
<DrawerTrigger render={<Button variant="secondary" />}>Notifications</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Notifications</DrawerTitle>
<DrawerDescription>Swipe right to dismiss.</DrawerDescription>
</DrawerHeader>
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2">
{notifications.map(({ title, detail, time }) => (
<div key={title} className="flex gap-3 rounded-md px-2 py-2 hover:bg-hover">
<BellIcon className="mt-0.5 size-4 shrink-0 text-icon-muted" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">{title}</p>
<p className="text-xs text-faint">{detail}</p>
</div>
<span className="text-xs text-faint">{time}</span>
</div>
))}
</div>
<DrawerFooter>
<DrawerClose render={<Button variant="secondary" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}Props
| Part | Extends |
|---|---|
| Drawer | Base UI's Drawer.Root |
| DrawerTrigger | Base UI's Drawer.Trigger |
| DrawerPortal | Base UI's Drawer.Portal |
| DrawerClose | Base UI's Drawer.Close |
| DrawerOverlay | Base UI's Drawer.Backdrop |
| DrawerSwipeHandle | <div> |
| DrawerContent | Base UI's Drawer.Popup |
| DrawerHeader | <div> |
| DrawerFooter | <div> |
| DrawerTitle | Base UI's Drawer.Title |
| DrawerDescription | Base UI's Drawer.Description |
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
Drawer
| Prop | Type | Default |
|---|---|---|
openWhether the drawer is currently open. | boolean | — |
defaultOpenWhether the drawer is initially open. To render a controlled drawer, use the | boolean | false |
modalDetermines if the drawer enters a modal state when open.
| "trap-focus" | boolean | true |
onOpenChangeEvent handler called when the drawer is opened or closed. | ((open: boolean, eventDetails: Drawer.Root.ChangeEventDetails) => void) | — |
onOpenChangeCompleteEvent handler called after any animations complete when the drawer is opened or closed. | ((open: boolean) => void) | — |
disablePointerDismissalWhether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it. | boolean | false |
actionsRefA ref to imperative actions.
| React.RefObject<DrawerRootActions | null> | — |
handleA handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method. | Drawer.Handle<unknown> | — |
triggerIdID of the trigger that the drawer is associated with.
This is useful in conjunction with the | string | null | — |
defaultTriggerIdID of the trigger that the drawer is associated with.
This is useful in conjunction with the | string | null | — |
childrenThe content of the drawer. | PayloadChildRenderFunction<unknown> | React.ReactNode | — |
swipeDirectionThe swipe direction used to dismiss the drawer. | "down" | "left" | "right" | "up" | "down" |
snapPointsSnap points used to position the drawer.
Use numbers between 0 and 1 to represent fractions of the viewport height,
numbers greater than 1 as pixel values, or strings in | DrawerSnapPoint[] | — |
snapToSequentialPointsDisables velocity-based snap skipping so drag distance determines the next snap point. | boolean | false |
snapPointThe currently active snap point. Use with | DrawerSnapPoint | null | — |
defaultSnapPointThe initial snap point value when uncontrolled. | DrawerSnapPoint | null | — |
onSnapPointChangeCallback fired when the snap point changes. | ((snapPoint: DrawerSnapPoint | null, eventDetails: Drawer.Root.SnapPointChangeEventDetails) => void) | — |
showSwipeHandle | boolean | false |
DrawerTrigger
| Prop | Type | Default |
|---|---|---|
handleA handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method. | Drawer.Handle<unknown> | — |
payloadA payload to pass to the drawer when it is opened. | unknown | — |
idID of the trigger. In addition to being forwarded to the rendered element,
it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root | string | — |
nativeButtonWhether the component renders a native | boolean | true |
DrawerPortal
| Prop | Type | Default |
|---|---|---|
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden. | boolean | false |
containerA parent element to render the portal element into. | HTMLElement | React.RefObject<HTMLElement | ShadowRoot | null> | ShadowRoot | null | — |
DrawerClose
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
DrawerOverlay
| Prop | Type | Default |
|---|---|---|
forceRenderWhether the backdrop is forced to render even when nested. | boolean | false |
DrawerContent
| Prop | Type | Default |
|---|---|---|
initialFocusDetermines the element to focus when the drawer is opened.
| boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void) | — |
finalFocusDetermines the element to focus when the drawer is closed.
| boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) | — |