React Dialog Component Examples
Drop-in React Dialog examples: confirmation prompt, controlled state, hidden close button, form dialog, nested dialogs. Built on the native <dialog>.
A modal dialog is the right surface for confirmations, focused forms, and any task that needs the user's attention before they continue. The Dialog 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 lines you own after install. Use it whenever the rest of the page should sit behind a backdrop while one task takes over.
This page collects the patterns that come up in shipped UIs. Each example is copy-paste ready and uses the same @/components/ui/dialog import path the Drivn CLI installs under, so the snippets compile the moment you run npx drivn add dialog. Drivn's Dialog uses dot notation — Dialog.Trigger opens, Dialog.Content renders the panel — and a title prop replaces the DialogHeader plus DialogTitle scaffold you might be used to.
If you want a side-anchored variant for mobile-friendly task flows, reach for Drawer. For an anchored, non-modal floating panel, use Popover. The Dialog examples below stay focused on the modal pattern: a basic confirmation, a controlled-state dialog, a chromeless dialog without the built-in close button, a form dialog that submits on action, and a nested dialog stacked on top of another.
Basic confirmation dialog
The default Dialog renders a Trigger button that opens a centered panel with a heading and your body content. Pass the title prop on Dialog.Content and the component renders an <h2> styled with text-lg font-semibold at the top of the panel. The close button (an X icon in the top-right corner) is rendered automatically — no extra subcomponent to import.
Because the Dialog 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 title prop, body content as plain JSX, and an action button. Outside-click closes the dialog because the component listens for clicks on the <dialog> backdrop element.
1 import { Dialog } from "@/components/ui/dialog" 2 import { Button } from "@/components/ui/button" 3 4 export default function Page() { 5 return ( 6 <Dialog> 7 <Dialog.Trigger>Open Dialog</Dialog.Trigger> 8 <Dialog.Content title="Confirm Action"> 9 <p>Are you sure you want to continue?</p> 10 <Button>Confirm</Button> 11 </Dialog.Content> 12 </Dialog> 13 ) 14 }
Controlled open state
For dialogs that need to open from outside the trigger — a side menu, a keyboard shortcut, or a route parameter — pass open and onOpenChange to the Dialog root. The component reconciles controlled and uncontrolled modes inside DialogRoot: 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 confirmation flows where the dialog must close only after an async action resolves. Keep the dialog open while the request is in flight, then call setOpen(false) from the action handler. The Dialog props reference lists every prop and the controlled API matches the names you may already use for shadcn or Radix dialogs.
1 import { useState } from "react" 2 import { Dialog } from "@/components/ui/dialog" 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)}>Edit profile</Button> 11 <Dialog open={open} onOpenChange={setOpen}> 12 <Dialog.Content title="Edit profile"> 13 <p>Form goes here.</p> 14 <Button onClick={() => setOpen(false)}>Save</Button> 15 </Dialog.Content> 16 </Dialog> 17 </> 18 ) 19 }
Form dialog with submit
Dialogs are the natural surface for short, focused forms — invite a teammate, rename a project, edit a single field. Render an Input inside Dialog.Content, hold the value in useState, and close the dialog from your submit handler after the request resolves. Because the dialog uses the native <dialog> element, focus enters the panel automatically and Escape cancels the form.
Keep the form vertical, leave breathing room above the action row, and place the primary action on the right. The Drivn Button variants — default for the primary action, outline for cancel — give the form the styling rhythm you see in the rest of the app. For multi-step forms or wizards, a Stepper inside the dialog body is a good fit.
1 import { useState } from "react" 2 import { Dialog } from "@/components/ui/dialog" 3 import { Button } from "@/components/ui/button" 4 import { Input } from "@/components/ui/input" 5 6 export default function Page() { 7 const [open, setOpen] = useState(false) 8 const [name, setName] = useState("") 9 10 const handleSubmit = async () => { 11 await fetch("/api/rename", { 12 method: "POST", 13 body: JSON.stringify({ name }), 14 }) 15 setOpen(false) 16 } 17 18 return ( 19 <Dialog open={open} onOpenChange={setOpen}> 20 <Dialog.Trigger>Rename project</Dialog.Trigger> 21 <Dialog.Content title="Rename project"> 22 <Input 23 value={name} 24 onChange={(e) => setName(e.target.value)} 25 placeholder="New name" 26 /> 27 <div className="flex gap-2 mt-4 justify-end"> 28 <Button variant="outline" onClick={() => setOpen(false)}> 29 Cancel 30 </Button> 31 <Button onClick={handleSubmit}>Save</Button> 32 </div> 33 </Dialog.Content> 34 </Dialog> 35 ) 36 }
Nested dialogs
The native <dialog> element manages a top-layer stack on the browser side, so opening one Dialog from inside another stacks them in z-order without manual portal mounting. Each Dialog.Content listens to its own context provider, and setOpen(false) on the inner one closes only the inner dialog. The browser returns focus to the outer dialog's previously focused element automatically.
Use nested dialogs sparingly — most of the time a single dialog with a multi-step body works better than two stacked ones. The right case is a confirmation that interrupts a longer task: an "Are you sure you want to discard?" prompt opened from inside an edit dialog, where confirming the inner dialog dismisses both. The example below shows the structure: an outer Dialog wraps the editor, an inner Dialog handles the confirmation.
1 import { useState } from "react" 2 import { Dialog } from "@/components/ui/dialog" 3 import { Button } from "@/components/ui/button" 4 5 export default function Page() { 6 const [outer, setOuter] = useState(false) 7 const [inner, setInner] = useState(false) 8 9 return ( 10 <Dialog open={outer} onOpenChange={setOuter}> 11 <Dialog.Trigger>Edit</Dialog.Trigger> 12 <Dialog.Content title="Edit document"> 13 <p>Unsaved changes will be lost.</p> 14 <Button onClick={() => setInner(true)}>Discard</Button> 15 <Dialog open={inner} onOpenChange={setInner}> 16 <Dialog.Content title="Discard changes?"> 17 <p>This cannot be undone.</p> 18 <Button 19 variant="destructive" 20 onClick={() => { 21 setInner(false) 22 setOuter(false) 23 }} 24 > 25 Discard 26 </Button> 27 </Dialog.Content> 28 </Dialog> 29 </Dialog.Content> 30 </Dialog> 31 ) 32 }
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 the side and is a better fit for mobile-friendly task flows or settings panels. Both trap focus and dismiss on Escape, but Dialog uses the native <dialog> element while Drawer is a positioned <aside>. Reach for Drawer on mobile-first surfaces; reach for Dialog when the task should clearly own the screen.
Switch to controlled mode by passing open and onOpenChange to the Dialog root, then call setOpen(false) from any handler — a Save button, a form submit, an async resolver. The pattern matches what you would do with a shadcn or Radix dialog. The built-in close button does the same thing internally, so a custom close button is just an additional caller of the same setter.
Yes — the native <dialog> element handles focus trap, Escape-to-close (via the cancel event), and top-layer stacking when the component calls el.showModal(). There is no Radix focus-scope or dismissable-layer module behind it. When the dialog closes, focus returns to the element that opened it, the same way a native modal would.
Switch to controlled mode and ignore the setOpen(false) calls that originate from the backdrop. The component fires onOpenChange(false) for outside-click, Escape, and the close button — gate which ones close the dialog inside your handler. For a one-step destructive confirmation, also pass showClose={false} so the corner icon disappears and only your action buttons can close it.
The Dialog file starts with "use client" because it manages state with useState and listens for events. 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.

