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.
Some tasks do not belong in the middle of the screen. Settings, filters, an edit form, a mobile menu — these read better anchored to an edge, with the page still visible behind a dimmed backdrop. That is what Drawer covers and Dialog does not. Drivn builds both on the same primitive: the native HTML <dialog> element, opened with showModal(), so the browser supplies top-layer stacking, the focus trap, and Escape-to-close. The Drawer file adds a little over two hundred lines of TSX on top of that and pulls in no runtime UI dependency.
Everything below is copy-paste ready against @/components/ui/drawer, the path the Drivn CLI installs to when you run npx drivn add drawer. The API is dot notation: Drawer.Trigger opens the panel, Drawer.Content takes a side of left, right, top, or bottom, and Drawer.Close, Drawer.Header, and Drawer.Footer fill in the chrome. Drawer.Header is worth calling out — title and description are string props, so the three-component header scaffold other libraries ask for collapses into a single tag. For an anchored, non-modal floating panel instead, use Popover.
Five patterns follow: a basic right-side panel, a controlled drawer opened from outside its trigger, one drawer per side, an edit form with pinned footer actions, and a mobile navigation menu.
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
Drawer.Content takes a side prop of right (the default), left, top, or bottom, and each value pulls one entry out of the styles.sides map in the registry source. Left and right panels get h-dvh w-full max-w-[400px] plus a border on the inner edge; top and bottom get w-screen h-[400px]. The motion is pure CSS — the resting position is a translate-x-[100%] or translate-y-[-100%] utility, group-open: snaps it back to [0%], and Tailwind 4's starting: variant supplies the from-value so the panel animates in on first paint instead of appearing already open. No JavaScript timer, no per-side branch in the component body.
Pick right for primary panels in left-to-right layouts, left for navigation that mirrors a sidebar, top for filters or announcements that belong above the content, and bottom for action sheets on touch surfaces. All four share the same 300 ms ease-out timing declared once in styles.content, so mixing sides across an app stays visually consistent. Drivn does not ship gesture-driven drag-to-dismiss — Drivn vs shadcn Drawer covers where that difference matters.
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.