Drawer
A swipeable panel from the bottom or side of the screen, built for touch, with snap points and nested drawers.
npx prodular-ui add drawerBottom
The default. Shows a swipe handle and dismisses with a downward swipe.
"use client"
import * as React from "react"import { CheckIcon } from "lucide-react"
import { Button } from "@/components/ui/button"import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer"
const stages = [ { id: "new", label: "New", count: 12 }, { id: "qualified", label: "Qualified", count: 8 }, { id: "proposal", label: "Proposal sent", count: 5 }, { id: "negotiation", label: "Negotiation", count: 3 }, { id: "won", label: "Closed won", count: 21 },]
export default function DrawerBottom() { const [stage, setStage] = React.useState("proposal") return ( <Drawer showSwipeHandle> <DrawerTrigger render={<Button variant="outline" />}>Move deal</DrawerTrigger> <DrawerContent> <div className="mx-auto flex w-full max-w-md flex-col"> <DrawerHeader> <DrawerTitle>Move Northgate renewal</DrawerTitle> <DrawerDescription>Pick the stage this deal is in now.</DrawerDescription> </DrawerHeader> <DrawerBody className="gap-px"> {stages.map((s) => ( <button key={s.id} type="button" onClick={() => setStage(s.id)} aria-pressed={stage === s.id} className="flex h-10 items-center gap-3 rounded-md px-3 text-left text-sm outline-none transition-colors duration-150 ease-out hover:bg-accent focus-visible:ring-3 focus-visible:ring-ring/40 aria-pressed:bg-accent aria-pressed:font-medium motion-reduce:transition-none" > <span className="flex-1">{s.label}</span> <span className="text-xs text-muted-foreground tabular-nums">{s.count}</span> <CheckIcon className={stage === s.id ? "size-4" : "invisible size-4"} /> </button> ))} </DrawerBody> <DrawerFooter> <DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose> <DrawerClose render={<Button />}>Move deal</DrawerClose> </DrawerFooter> </div> </DrawerContent> </Drawer> )}Side
swipeDirection right turns it into a side panel that closes with a swipe.
import { Button } from "@/components/ui/button"import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@/components/ui/drawer"import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"import { Switch } from "@/components/ui/switch"
const prefs = [ { label: "Mentions", description: "When someone writes @you in a comment.", on: true }, { label: "Assigned to you", description: "New issues and reassignments.", on: true }, { label: "Status changes", description: "Only for issues you created.", on: false }, { label: "Weekly digest", description: "A summary every Monday at 9:00.", on: true },]
export default function DrawerSide() { return ( <Drawer swipeDirection="right"> <DrawerTrigger render={<Button variant="outline" />}>Notification settings</DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Notifications</DrawerTitle> <DrawerDescription>Choose what reaches your inbox. Swipe right to close.</DrawerDescription> </DrawerHeader> <DrawerBody className="pt-5"> <FieldGroup className="gap-5"> {prefs.map((p) => ( <Field key={p.label} orientation="horizontal"> <FieldContent> <FieldLabel>{p.label}</FieldLabel> <FieldDescription>{p.description}</FieldDescription> </FieldContent> <Switch defaultChecked={p.on} /> </Field> ))} </FieldGroup> </DrawerBody> <DrawerFooter> <DrawerClose render={<Button />}>Done</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> )}Anatomy
<Drawer showSwipeHandle> <DrawerTrigger render={<Button variant="outline" />}>Move deal</DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Move deal</DrawerTitle> <DrawerDescription>Pick the stage this deal is in now.</DrawerDescription> </DrawerHeader> <DrawerBody>{/* options */}</DrawerBody> <DrawerFooter> <DrawerClose render={<Button />}>Done</DrawerClose> </DrawerFooter> </DrawerContent></Drawer>Guidance
Use it when
- Use on mobile and touch first surfaces for pickers, filters and short forms.
- Use showSwipeHandle on bottom drawers so people know they can swipe it away.
- Use snapPoints when the drawer has a peek state and a full state.
Avoid
- Desktop editing flows. Use Sheet, which has no swipe physics.
- Long forms. Content taller than the viewport fights with the swipe gesture.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Always render DrawerTitle inside DrawerHeader.
- Set swipeDirection on Drawer, not on DrawerContent. down is the default.
- Compose as DrawerHeader, DrawerBody, DrawerFooter. Put scrolling content in DrawerBody.
- On bottom drawers, constrain the inner content with mx-auto max-w-md so it reads well on tablets.
API
Drawer
| Prop | Type | Default | Description |
|---|---|---|---|
| swipeDirection | "down" | "up" | "left" | "right" | "down" | Direction that dismisses the drawer. Also sets the edge it opens from. |
| showSwipeHandle | boolean | false | Renders the grab handle. |
| snapPoints | (number | string)[] | Heights the drawer settles at. | |
| modal | boolean | true | Shows the backdrop and traps focus. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add drawer# or, with the registry in components.jsonnpx shadcn@latest add @prodular/drawer