Dialog
A modal window for a focused task, such as a short form or a review step, that blocks the page until it is done or dismissed.
npx prodular-ui add dialogForm
Header, fields and a footer with the primary action on the right.
import { UserPlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog"import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"import { Input } from "@/components/ui/input"import { Textarea } from "@/components/ui/textarea"
export default function DialogBasic() { return ( <Dialog> <DialogTrigger render={<Button variant="outline" />}> <UserPlusIcon data-icon="inline-start" /> Invite teammates </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Invite teammates to Halcyon</DialogTitle> <DialogDescription> They get access to every shared project. You can change roles later in Settings. </DialogDescription> </DialogHeader> <FieldGroup className="gap-4"> <Field> <FieldLabel>Email addresses</FieldLabel> <Input placeholder="priya@halcyon.dev, tom@halcyon.dev" /> <FieldDescription>Separate addresses with a comma.</FieldDescription> </Field> <Field> <FieldLabel>Personal note</FieldLabel> <Textarea rows={3} defaultValue="Hey, we're moving the roadmap into Relay this week. Join us here." /> </Field> </FieldGroup> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose> <Button>Send invites</Button> </DialogFooter> </DialogContent> </Dialog> )}Sizes
sm for one input or a rename, default for most forms, lg for content that needs width.
import { Button } from "@/components/ui/button"import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog"
const sizes = [ { size: "sm", label: "Small", title: "Rename view", description: "Views are shared with everyone in the workspace.", }, { size: "default", label: "Default", title: "Move 3 issues", description: "Choose a project. Labels and assignees stay the same, cycles are cleared.", }, { size: "lg", label: "Large", title: "Connect a data source", description: "Relay reads from your warehouse on a schedule. Nothing is written back, and you can disconnect at any time from Settings.", },] as const
export default function DialogSizes() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> {sizes.map((s) => ( <Dialog key={s.size}> <DialogTrigger render={<Button variant="outline" />}>{s.label}</DialogTrigger> <DialogContent size={s.size}> <DialogHeader> <DialogTitle>{s.title}</DialogTitle> <DialogDescription>{s.description}</DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose> <Button>Continue</Button> </DialogFooter> </DialogContent> </Dialog> ))} </div> )}Long content
Wrap the middle in DialogBody so it scrolls while the header and footer stay in place.
import { Button } from "@/components/ui/button"import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog"
const notes = [ { title: "Saved views for every team", body: "Pin a filter set to the sidebar and share it with your team. Views keep their sort order, grouping and visible columns.", }, { title: "Faster search", body: "Search now returns results in under 80 ms on workspaces with up to two million issues. Typos are forgiven and recent items rank first.", }, { title: "SLA timers on tickets", body: "Set a first response and a resolution target per priority. Tickets that are about to breach move to the top of the inbox.", }, { title: "Bulk edit from the keyboard", body: "Select rows with Shift and the arrow keys, then press E to change status, assignee or labels for all of them at once.", }, { title: "Audit log export", body: "Admins on the Business plan can export the full audit log as CSV for any date range in the last 13 months.", }, { title: "Fixes", body: "Attachments over 25 MB no longer fail silently. Mentions in code blocks stop sending notifications. The Slack bot keeps thread context after edits.", },]
export default function DialogScrolling() { return ( <Dialog> <DialogTrigger render={<Button variant="outline" />}>What's new</DialogTrigger> <DialogContent size="lg" className="max-h-[min(36rem,calc(100dvh-2rem))]"> <DialogHeader> <DialogTitle>Relay 4.2 release notes</DialogTitle> <DialogDescription>Shipped on October 14. Rolling out to all workspaces this week.</DialogDescription> </DialogHeader> <DialogBody className="divide-y gap-0"> {notes.map((n) => ( <section key={n.title} className="flex flex-col gap-1 py-4 first:pt-0 last:pb-0"> <h3 className="text-sm font-medium text-foreground">{n.title}</h3> <p className="text-sm text-muted-foreground">{n.body}</p> </section> ))} </DialogBody> <DialogFooter> <Button variant="link" className="mr-auto"> Read the changelog </Button> <DialogClose render={<Button />}>Got it</DialogClose> </DialogFooter> </DialogContent> </Dialog> )}Anatomy
<Dialog> <DialogTrigger render={<Button variant="outline" />}>Invite teammates</DialogTrigger> <DialogContent size="default"> <DialogHeader> <DialogTitle>Invite teammates</DialogTitle> <DialogDescription>They get access to every shared project.</DialogDescription> </DialogHeader> <DialogBody>{/* fields */}</DialogBody> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose> <Button>Send invites</Button> </DialogFooter> </DialogContent></Dialog>Guidance
Use it when
- Use for a short task that must be finished or cancelled before the user goes back to the page: invite people, rename, connect an integration.
- Use size sm for a single field, default for forms up to about six fields, lg for previews, tables or two column layouts.
- Put the primary action last in DialogFooter so it sits on the right, with Cancel to its left.
Avoid
- Confirming a destructive action. Use Alert dialog, which cannot be dismissed by clicking outside.
- Long multi step flows or anything the user wants to compare with the page behind. Use a Sheet or a full page.
- Nesting cards or tinted panels inside the dialog. Separate sections with border-t or divide-y.
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 DialogTitle. If the design has no visible title, keep it and add className="sr-only".
- Add DialogDescription when the title alone does not explain what happens.
- Open from a DialogTrigger with render={<Button />}, or control it with open and onOpenChange. Do not toggle it with conditional rendering.
- Use DialogClose render={<Button variant="outline" />} for Cancel. Do not write your own close handler for it.
- Wrap long content in DialogBody. Never make the whole DialogContent scroll.
- Pick size from the content: sm, default or lg. Do not set a max-w class yourself unless a size does not fit.
API
Dialog
| Prop | Type | Default | Description |
|---|---|---|---|
| open / onOpenChange | boolean / (open: boolean) => void | Controlled state. Omit both for uncontrolled with defaultOpen. |
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "default" | "lg" | "default" | Max width: 24rem, 32rem or 42rem. |
| showCloseButton | boolean | true | Renders the icon button in the top right corner. |
DialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
| showCloseButton | boolean | false | Adds an outline Close button before the other actions. |
DialogTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| render | ReactElement | Renders the trigger as another element, usually a Button. |
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 dialog# or, with the registry in components.jsonnpx shadcn@latest add @prodular/dialog