React Drawer Component Examples
Drop-in React Drawer examples: right slide-in, controlled state, four sides, edit form, navigation drawer. Built on the native <dialog> with zero deps.
A drawer is the right surface for settings panels, navigation, edit forms, and any task that benefits from a side-anchored slide-in instead of a centered Dialog. The Drawer component in Drivn wraps the native HTML <dialog> element, so the browser handles modal stacking, focus trap, and Escape-to-close — the component file ships zero runtime UI dependencies and weighs around a hundred and fifty lines you own after install. Use it whenever the rest of the page should sit behind a backdrop while one focused task slides in from an edge.
This page collects the patterns that come up in shipped UIs. Each example is copy-paste ready and uses the same @/components/ui/drawer import path the Drivn CLI installs under, so the snippets compile the moment you run npx drivn add drawer. Drivn's Drawer uses dot notation — Drawer.Trigger opens, Drawer.Content renders the panel — and the side prop on Drawer.Content accepts left, right, top, or bottom. The Drawer.Header subcomponent takes title and description as string props, replacing the DrawerHeader plus DrawerTitle plus DrawerDescription scaffold you might be used to from other libraries.
If you want a centered modal instead of a side-anchored panel, reach for Dialog. For an anchored, non-modal floating panel, use Popover. The Drawer examples below stay focused on the slide-in pattern: a basic right drawer, a controlled-state drawer driven from outside the trigger, a drawer for every side, a drawer with an edit form and footer actions, and a navigation drawer for mobile menus.
Basic right-side drawer
The default Drawer renders a Trigger button that opens a panel sliding in from the right edge. Pass the title and description props on Drawer.Header and the component renders an <h2> styled with text-lg font-semibold plus a muted description line. The close button is rendered via Drawer.Close — drop it in the top-right corner and the registry source positions it absolutely with a hover state.
Because the Drawer source lives in your repo after install, swapping the trigger styling, panel max-width, or the close icon is a local edit. The minimum viable integration is a Trigger plus a Content with a side prop, a Header for the title row, and your body content as plain JSX. Outside-click closes the drawer because the component listens for clicks on the <dialog> backdrop, and Escape works through the native cancel event.
1 import { Drawer } from "@/components/ui/drawer" 2 import { Button } from "@/components/ui/button" 3 4 export default function Page() { 5 return ( 6 <Drawer> 7 <Drawer.Trigger>Edit profile</Drawer.Trigger> 8 <Drawer.Content side="right"> 9 <Drawer.Close /> 10 <Drawer.Header 11 title="Edit profile" 12 description="Make changes to your profile." 13 /> 14 <div className="flex-1 p-6"> 15 <p>Form content goes here.</p> 16 </div> 17 <Drawer.Footer> 18 <Button>Save</Button> 19 </Drawer.Footer> 20 </Drawer.Content> 21 </Drawer> 22 ) 23 }
Controlled open state
For drawers that need to open from outside the trigger — a keyboard shortcut, a route parameter, a "notification arrived" toast — pass open and onOpenChange to the Drawer root. The component reconciles controlled and uncontrolled modes inside DrawerRoot: when open is undefined, internal useState runs; when defined, the prop wins and onOpenChange fires alongside any internal updates.
This pattern is also the right one for edit drawers where the panel must close only after an async save resolves. Keep the drawer open while the request is in flight, then call setOpen(false) from the action handler. The Drawer props reference lists every prop, and the controlled API matches every other Drivn compound — Dialog, Popover, Dropdown all accept the same open plus onOpenChange pair.
1 import { useState } from "react" 2 import { Drawer } from "@/components/ui/drawer" 3 import { Button } from "@/components/ui/button" 4 5 export default function Page() { 6 const [open, setOpen] = useState(false) 7 8 return ( 9 <> 10 <Button onClick={() => setOpen(true)}>Open drawer</Button> 11 <Drawer open={open} onOpenChange={setOpen}> 12 <Drawer.Content side="right"> 13 <Drawer.Close /> 14 <Drawer.Header title="Settings" /> 15 <div className="flex-1 p-6"> 16 <p>Drawer body.</p> 17 </div> 18 <Drawer.Footer> 19 <Button onClick={() => setOpen(false)}>Done</Button> 20 </Drawer.Footer> 21 </Drawer.Content> 22 </Drawer> 23 </> 24 ) 25 }
All four sides
The side prop on Drawer.Content accepts right (default), left, top, or bottom. Each value picks the right translate utility from the styles.sides map in the registry source — side drawers default to a 400 px max-width and full viewport height, top and bottom drawers default to 400 px height and full viewport width. The animation uses Tailwind 4's starting: and group-open: variants on translate, so the slide-in direction comes from CSS, not a JavaScript timer.
Use right for primary panels in left-to-right layouts, left for navigation drawers that pair with a sidebar, top for global announcements or filters above the page, and bottom for action sheets on touch surfaces. If you want a true gesture-driven bottom sheet with drag-to-dismiss, Drivn does not ship that — see Drivn vs shadcn Drawer for the comparison.
1 <Drawer> 2 <Drawer.Trigger>Right</Drawer.Trigger> 3 <Drawer.Content side="right"> 4 <Drawer.Close /> 5 <div className="p-6">Slides from right</div> 6 </Drawer.Content> 7 </Drawer> 8 9 <Drawer> 10 <Drawer.Trigger>Left</Drawer.Trigger> 11 <Drawer.Content side="left"> 12 <Drawer.Close /> 13 <div className="p-6">Slides from left</div> 14 </Drawer.Content> 15 </Drawer> 16 17 <Drawer> 18 <Drawer.Trigger>Top</Drawer.Trigger> 19 <Drawer.Content side="top"> 20 <Drawer.Close /> 21 <div className="p-6">Slides from top</div> 22 </Drawer.Content> 23 </Drawer> 24 25 <Drawer> 26 <Drawer.Trigger>Bottom</Drawer.Trigger> 27 <Drawer.Content side="bottom"> 28 <Drawer.Close /> 29 <div className="p-6">Slides from bottom</div> 30 </Drawer.Content> 31 </Drawer>
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Dialog renders as a centered modal panel anchored in the middle of the viewport. Drawer slides in from one of four edges and is a better fit for navigation menus, edit forms, and settings panels. Both trap focus and dismiss on Escape, and both are built on the native <dialog> element — they share the same primitive and the same controlled API. Reach for Drawer when the task lives at the edge of the screen; reach for Dialog when it should sit dead-center.
Switch to controlled mode by passing open and onOpenChange to the Drawer root, then call setOpen(false) from any handler — a Save button, a form submit, an async resolver. The built-in close button does the same thing internally, so a custom close action is just an additional caller of the same setter. The pattern matches every other Drivn compound including Dialog and Popover.
No — Drivn Drawer is a side-anchored slide-in, not a gesture-driven bottom sheet. It dismisses on outside-click, Escape, or the built-in close button. If drag-to-dismiss is critical to your UX (iOS-style bottom sheets with snap points), the shadcn/ui Drawer wraps Vaul and is the right pick for that surface. See Drivn vs shadcn Drawer for a full comparison.
Yes — pass side="bottom" to Drawer.Content and the panel slides up from the bottom edge with a default height of 400 px and full viewport width. The animation uses the same translate-y plus starting: variant pattern that powers the side variants, so the timing and easing match. For a true gesture-driven bottom sheet with drag-to-dismiss, Drivn does not ship that — use Vaul through shadcn/ui for that specific surface.
The Drawer file starts with "use client" because it manages state with useState and listens for events on the <dialog> element. You can import it from a Server Component, but it renders on the client. Next.js draws the client boundary correctly based on the use client directive at the top of the file — no manual configuration needed in your layout or page. Render the trigger and content inside any server-rendered page and the drawer hydrates on the client automatically.

