Shadcn Dialog
Free shadcn/ui dialog (modal) component for React — sizes, controlled open, form-in-dialog, scrollable content and prevent-close. Copy or install in one command.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
open
= —
boolean
Controlled open state (on Dialog). Pair with onOpenChange.
onOpenChange
= —
(open: boolean) => void
Fires when the dialog opens or closes (on Dialog).
showCloseButton
= true
boolean
Show the built-in X close button (on DialogContent). Set false to hide it.
Ten composable parts (Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, DialogClose, DialogOverlay, DialogPortal). Size it with a className on DialogContent (e.g. sm:max-w-xl). Prevent dismissal via onInteractOutside / onEscapeKeyDown preventDefault.
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
Examples
Basic
A trigger that opens a centered modal with a `DialogHeader` (title + description) and a `DialogFooter` of actions.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export default function DialogDemo01() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Share document</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Share this document</DialogTitle>
<DialogDescription>
Anyone with the link can view this document.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Copy link</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
With form
A controlled edit-profile form inside the dialog that closes itself on submit via `onOpenChange`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function DialogForm01() {
const [open, setOpen] = React.useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent>
<form
onSubmit={(event) => {
event.preventDefault()
setOpen(false)
}}
className="grid gap-4"
>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're
done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="Ada Lovelace" />
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input id="username" defaultValue="@ada" />
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
Sizes
Small, default, large, and extra-large dialogs — width is set with a `className` on `DialogContent` (e.g. `sm:max-w-xl`), since there is no size prop.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
const sizes = [
{ label: "Small", className: "sm:max-w-sm" },
{ label: "Default", className: "sm:max-w-lg" },
{ label: "Large", className: "sm:max-w-xl" },
{ label: "Extra large", className: "sm:max-w-3xl" },
]
export default function DialogSizes01() {
return (
<div className="flex flex-wrap items-center gap-3">
{sizes.map((size) => (
<Dialog key={size.label}>
<DialogTrigger asChild>
<Button variant="outline">{size.label}</Button>
</DialogTrigger>
<DialogContent className={size.className}>
<DialogHeader>
<DialogTitle>{size.label} dialog</DialogTitle>
<DialogDescription>
This dialog uses <code>{size.className}</code> to set its width.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button>Got it</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
))}
</div>
)
}
Scrollable
Long content scrolls between a sticky header and footer — cap the height with `max-h` and make the body `overflow-y-auto`.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
const paragraphs = Array.from({ length: 10 }, (_, i) => i + 1)
export default function DialogScroll01() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Terms of Service</Button>
</DialogTrigger>
<DialogContent className="flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-lg">
<DialogHeader className="shrink-0 border-b px-6 py-4 text-left">
<DialogTitle>Terms of Service</DialogTitle>
</DialogHeader>
<div className="overflow-y-auto px-6 py-4 text-sm leading-relaxed text-muted-foreground">
{paragraphs.map((n) => (
<p key={n} className={n > 1 ? "mt-4" : undefined}>
{n}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</p>
))}
</div>
<DialogFooter className="shrink-0 border-t px-6 py-4">
<DialogClose asChild>
<Button variant="outline">Decline</Button>
</DialogClose>
<DialogClose asChild>
<Button>Accept</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Controlled
Opened and closed entirely from React state with no `DialogTrigger` — the pattern for opening a dialog programmatically from anywhere.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
export default function DialogControlled01() {
const [open, setOpen] = React.useState(false)
return (
<>
<Button variant="outline" onClick={() => setOpen(true)}>
Open from anywhere
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Controlled dialog</DialogTitle>
<DialogDescription>
This dialog has no DialogTrigger — it's opened and closed
entirely from React state.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={() => setOpen(false)}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
Prevent dismiss
Hide the X with `showCloseButton={false}` and block outside-click / Escape via `onInteractOutside` and `onEscapeKeyDown` `preventDefault` — forcing an explicit choice.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export default function DialogCustomClose01() {
const [open, setOpen] = React.useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Open</Button>
</DialogTrigger>
<DialogContent
showCloseButton={false}
onInteractOutside={(event) => event.preventDefault()}
onEscapeKeyDown={(event) => event.preventDefault()}
>
<DialogHeader>
<DialogTitle>Unsaved changes</DialogTitle>
<DialogDescription>
No close button, and clicking outside or pressing Escape won't
dismiss this — you have to choose an action.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Discard
</Button>
<Button onClick={() => setOpen(false)}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Confirmation
A destructive confirmation. For confirmations specifically, an Alert Dialog is the more semantic choice.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export default function DialogConfirmation01() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This permanently deletes your account and removes your data from our
servers. This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive">Delete account</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
The Shadcn Dialog is the modal at the center of your app — the canonical shadcn/ui dialog built on Radix, with a focus-trapped overlay, fade-and-zoom animation, and a built-in close button. It composes from DialogTrigger, DialogContent, DialogHeader/DialogTitle/DialogDescription, and DialogFooter. The examples below cover the patterns the official docs leave to you — sizing, controlled open, scrollable content, and preventing dismissal.
When to use — Dialog vs Alert Dialog vs Sheet
Use a Dialog for general modal content: a form, a detail view, settings. Use an Alert Dialog for an interrupting confirmation that needs a deliberate yes/no (delete, discard) — it has no quick dismiss and is announced as an alert. Use a Sheet or Drawer when the panel should slide from an edge — better for navigation and on mobile. A Button is the usual trigger.
Sizing
There is no size prop — set the width with a className on DialogContent: sm:max-w-sm for narrow, the default sm:max-w-lg, or sm:max-w-3xl for wide. The dialog is already responsive (max-w-[calc(100%-2rem)] keeps a margin on small screens). For tall content, add max-h-[80vh] and make the scrolling region overflow-y-auto — see the Sizes and Scrollable examples.
Controlled open and closing
For anything beyond a simple trigger, control the dialog: pass open and onOpenChange and drive them from state. That lets you open it programmatically from anywhere (no DialogTrigger) and close it from code — the classic case is closing after a form submits successfully. The Controlled and With form examples show both.
Preventing dismissal
By default the dialog closes on the X, outside-click, and Escape. To force an explicit choice — an unsaved-changes guard — hide the X with showCloseButton={false} and call event.preventDefault() in onInteractOutside and onEscapeKeyDown. The Prevent dismiss example demonstrates it.
Accessibility
Radix handles the hard parts: focus moves into the dialog on open and is trapped while it's open, returns to the trigger on close, the background is inert, and Escape closes it. Always include a DialogTitle (visually hidden if needed) so screen readers announce the dialog, and a DialogDescription for context.
Theming
The dialog reads your design tokens (--background, --border, --ring, and the overlay's black/50), so it follows your theme — including dark mode — with zero overrides.