# 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.

Source: https://designrevision.com/components/dialog

The **Shadcn Dialog** is the modal at the center of your app — the canonical <a href="https://ui.shadcn.com/docs/components/dialog" rel="nofollow">shadcn/ui</a> dialog built on <a href="https://www.radix-ui.com/primitives/docs/components/dialog" rel="nofollow">Radix</a>, 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](/components/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.

## Installation

### dialog

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog
```

**Dependencies:** @radix-ui/react-dialog, lucide-react, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| open | boolean | — | Controlled open state (on Dialog). Pair with onOpenChange. |
| onOpenChange | (open: boolean) => void | — | Fires when the dialog opens or closes (on Dialog). |
| showCloseButton | boolean | true | Show the built-in X close button (on DialogContent). Set false to hide it. |

**Usage & accessibility:** 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.

```tsx
// components/ui/dialog.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-demo-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-demo-01.tsx
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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-form-01
```

**Dependencies:** dialog, button, input, label

```tsx
// components/ui/dialog-form-01.tsx
"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&apos;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.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-sizes-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-sizes-01.tsx
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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-scroll-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-scroll-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-controlled-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-controlled-01.tsx
"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&apos;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.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-custom-close-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-custom-close-01.tsx
"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&apos;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.

**Install:**

```bash
npx shadcn@latest add @designrevision/dialog-confirmation-01
```

**Dependencies:** dialog, button

```tsx
// components/ui/dialog-confirmation-01.tsx
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>
  )
}
```
