# Dialog

The lights go down and a panel rises onto the desk, screwed or stitched at its corners. Content has no padding of its own, so it hosts forms and command palettes alike; AlertDialog asks before something can't be undone.

- Category: Essentials
- Docs: https://bevel-ui.vercel.app/components/dialog
- Import from: `@/components/ui/dialog`

## Install

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

npm dependencies (installed by the CLI): `@base-ui/react`, `cn`.

## Anatomy

```tsx
<Dialog>
  <DialogTrigger />
  <DialogContent>
    <DialogHeader>
      <DialogTitle />
      <DialogDescription />
    </DialogHeader>
    <DialogBody />
    <DialogFooter>
      <DialogClose />
    </DialogFooter>
  </DialogContent>
</Dialog>

<AlertDialog>
  <AlertDialogTrigger />
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle />
      <AlertDialogDescription />
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel />
      <AlertDialogAction />
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

## Parts

| Part | Renders | Description |
| --- | --- | --- |
| `Dialog` | Dialog root | open, defaultOpen, onOpenChange, modal. Escape, the backdrop and Close put it away; focus returns to the trigger. |
| `DialogTrigger` | button | Opens the dialog. Pass render to use your own button. |
| `DialogContent` | Dialog popup | Dims the room and raises the panel. No padding. align: center or top (for a palette). fasteners: screws (default), stitch, none. showCloseButton (default true). Width from --dialog-width; override with className. |
| `DialogHeader` | div | Title and description, padded, clear of the corner ×. |
| `DialogTitle` | h2 | Names the dialog for screen readers. Required; hide it with sr-only if it must not show. |
| `DialogDescription` | p | Muted text under the title. |
| `DialogBody` | div | Padded; scrolls when the panel runs out of room. |
| `DialogFooter` | div | A ledge behind an engraved rule, holding the keys. Stacks on small screens. |
| `DialogClose` | button | A raised key that closes. tone: default or destructive. |
| `AlertDialog` | Alert dialog root | Doesn't close on an outside click. |
| `AlertDialogContent` | Alert dialog popup | The same panel, narrower (--dialog-alert-width), with no corner ×. fasteners. |
| `AlertDialogCancel` | button | The safe answer. Put it first: it takes focus. Says “Cancel” by default. |
| `AlertDialogAction` | button | Does it and closes; onClick runs first. A red key by default (tone="destructive"). |

## Example: Delete a project

An alert dialog with a red key. Cancel takes focus, and a stray click outside does nothing.

```tsx
"use client"

import * as React from "react"
import { Trash2 } from "lucide-react"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/dialog"

/** Deleting a project: an alert dialog that won't close on a stray click. Cancel takes focus first. */
export function DialogDeleteProject({ project = "acme-web" }: { project?: string }) {
  const [deleted, setDeleted] = React.useState(false)
  return (
    <div className="flex flex-col items-center gap-4">
      <AlertDialog>
        <AlertDialogTrigger
          disabled={deleted}
          className="inline-flex h-8 items-center gap-1.5 rounded-(--bevel-radius-sm) border border-destructive/30 bg-background px-3 text-sm font-medium text-destructive outline-none hover:bg-destructive/10 focus-visible:ring-2 focus-visible:ring-destructive/40 disabled:opacity-50"
        >
          <Trash2 className="size-4" aria-hidden />
          Delete project
        </AlertDialogTrigger>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete {project}?</AlertDialogTitle>
            <AlertDialogDescription>
              Its 14 deployments, environment variables and domains go with it. This can&rsquo;t be undone.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel />
            <AlertDialogAction onClick={() => setDeleted(true)}>Delete project</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <p role="status" className="text-sm text-muted-foreground">
        {deleted ? `${project} was deleted.` : `${project} · 14 deployments`}
      </p>
      {deleted && (
        <button type="button" onClick={() => setDeleted(false)} className="text-sm underline underline-offset-4">
          Restore for the demo
        </button>
      )}
    </div>
  )
}
```

## Example: A form

Rename a document. Enter saves; Escape, × or Cancel leave it.

```tsx
"use client"

import * as React from "react"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

/** A small form in a dialog. Enter saves; Escape, the × or Cancel leave it as it was. */
export function DialogRename() {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState("Quarterly report")
  const fieldId = React.useId()

  function save(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const value = new FormData(event.currentTarget).get("name")?.toString().trim()
    if (value) setName(value)
    setOpen(false)
  }

  return (
    <div className="flex flex-col items-center gap-4">
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger className="inline-flex h-8 items-center rounded-(--bevel-radius-sm) border bg-background px-3 text-sm font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring">
          Rename
        </DialogTrigger>
        <DialogContent>
          <form onSubmit={save} className="contents">
            <DialogHeader>
              <DialogTitle>Rename document</DialogTitle>
              <DialogDescription>Everyone with access sees the new name.</DialogDescription>
            </DialogHeader>
            <DialogBody>
              <label htmlFor={fieldId} className="mb-1.5 block text-sm font-medium">
                Name
              </label>
              <input
                id={fieldId}
                name="name"
                defaultValue={name}
                autoComplete="off"
                className="h-9 w-full rounded-(--bevel-radius-sm) border border-current/15 bg-transparent px-3 text-base shadow-(--bevel-depth-pressed) outline-none focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/50 sm:text-sm"
              />
            </DialogBody>
            <DialogFooter>
              <DialogClose>Cancel</DialogClose>
              <button
                type="submit"
                className="inline-flex h-8 items-center justify-center rounded-(--bevel-radius-sm) bg-foreground px-3 text-sm font-medium text-background outline-none focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/60 focus-visible:ring-offset-1"
              >
                Save
              </button>
            </DialogFooter>
          </form>
        </DialogContent>
      </Dialog>
      <p className="text-sm text-muted-foreground">{name}</p>
    </div>
  )
}
```

## Example: Command palette

⌘K opens a palette in the dialog: top-aligned, no padding, no fasteners.

```tsx
"use client"

import * as React from "react"
import { Autocomplete } from "@base-ui/react/autocomplete"
import { Search } from "lucide-react"
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"

const commands = [
  "New document",
  "Open recent",
  "Share page",
  "Copy link",
  "Toggle dark mode",
  "Go to settings",
  "Invite a teammate",
  "Sign out",
]

/** A ⌘K command palette hosted in the dialog: top-aligned, no padding, no corner ×. */
export function DialogCommand() {
  const [open, setOpen] = React.useState(false)
  const [ran, setRan] = React.useState<string | null>(null)

  React.useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault()
        setOpen((o) => !o)
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <div className="flex flex-col items-center gap-4">
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger className="inline-flex h-8 w-56 max-w-full items-center gap-2 rounded-(--bevel-radius-sm) border bg-background px-2.5 text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
          <Search className="size-4" aria-hidden />
          Run a command…
          <kbd aria-hidden className="ml-auto rounded-sm border px-1.5 font-mono text-xs">
            ⌘K
          </kbd>
        </DialogTrigger>
        <DialogContent align="top" fasteners="none" showCloseButton={false} className="max-w-lg">
          <DialogTitle className="sr-only">Command palette</DialogTitle>
          <Autocomplete.Root open inline items={commands} autoHighlight="always" keepHighlight>
            <div className="flex items-center gap-2 border-b border-current/10 px-4">
              <Search className="size-4 shrink-0 opacity-60" aria-hidden />
              <Autocomplete.Input
                aria-label="Search commands"
                placeholder="Search commands…"
                className="h-12 w-full min-w-0 bg-transparent text-base outline-none placeholder:opacity-60 sm:text-sm"
              />
            </div>
            <div className="max-h-72 overflow-y-auto overscroll-contain p-2">
              <Autocomplete.Empty>
                <p className="px-3 py-8 text-center text-sm opacity-60">No commands match.</p>
              </Autocomplete.Empty>
              <Autocomplete.List>
                {(command: string) => (
                  <Autocomplete.Item
                    key={command}
                    value={command}
                    onClick={() => {
                      setRan(command)
                      setOpen(false)
                    }}
                    className="flex h-9 cursor-default items-center rounded-(--bevel-radius-sm) px-3 text-sm outline-none select-none data-highlighted:bg-current/8"
                  >
                    {command}
                  </Autocomplete.Item>
                )}
              </Autocomplete.List>
            </div>
          </Autocomplete.Root>
        </DialogContent>
      </Dialog>
      <p role="status" className="text-sm text-muted-foreground">
        {ran ? `Ran “${ran}”.` : "Press ⌘K or Ctrl K."}
      </p>
    </div>
  )
}
```
