<!-- Protocore Design System — Dialog -->
# Dialog

- **Category:** Overlay (`overlay`)
- **Slug:** `overlay/dialog`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Dialog } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/overlay/dialog

> A modal panel over a dimmed backdrop for focused tasks — forms, confirmations, and detail views that demand attention.

## When to use it

A Dialog interrupts the flow for a **self-contained task** the user opts into: an edit form, a multi-step choice, a focused detail view. It dims the page, traps focus, and closes on Esc or backdrop click.

For a **destructive or irreversible confirmation**, use **[AlertDialog](/overlay/alert-dialog)** — or its `ConfirmDialog` convenience — which removes the backdrop-dismiss escape hatch so the choice is deliberate. For a **long detail panel or side form** that shouldn't cover the whole screen, use a **[Sheet](/overlay/sheet)**. For lightweight, non-modal floating content anchored to a control, use a **[Popover](/overlay/popover)**. Reserve the Dialog for moments that genuinely warrant blocking the rest of the UI.

## Compound parts

Dialog is a compound: **`Dialog.Root`** (state) · **`Dialog.Trigger`** · **`Dialog.Content`** (backdrop + panel, renders the close × unless `showClose={false}`) · **`Dialog.Title`** (required) · **`Dialog.Description`** · **`Dialog.Footer`** (right-aligned action row) · **`Dialog.Close`**. Compose only the parts you need — but always include a `Title`, or pass `aria-describedby={undefined}` when you deliberately omit a `Description`.

## Props

### Dialog.Content

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | no | — | — |
| `className` | `string` | no | — | — |
| `showClose` | `boolean` | no | `true` | Render a top-right 32px ghost × close button. |
| `style` | `CSSProperties` | no | — | — |

### Dialog.Footer

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Basics

Compose the parts: `Dialog.Root` owns open state, `Dialog.Trigger` (via `asChild`) opens it, and `Dialog.Content` portals the backdrop + centered panel. A `Title` is required for accessibility; `Dialog.Close` dismisses.

```tsx
import { Dialog, Button } from "@protocore/pds";

export default function DialogBasics() {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <Button>Invite operator</Button>
      </Dialog.Trigger>
      <Dialog.Content style={{ width: 440 }}>
        <Dialog.Title>Invite operator</Dialog.Title>
        <Dialog.Description>
          They will receive read-write access to the staging cluster and can rotate signing keys.
        </Dialog.Description>
        <Dialog.Footer>
          <Dialog.Close asChild>
            <Button variant="secondary">Cancel</Button>
          </Dialog.Close>
          <Dialog.Close asChild>
            <Button>Send invite</Button>
          </Dialog.Close>
        </Dialog.Footer>
      </Dialog.Content>
    </Dialog.Root>
  );
}
```

### With a form

Everything inside `Dialog.Content` is yours to compose — stack Fields and Inputs, then wrap each footer action in `Dialog.Close asChild` so a click closes the dialog. Focus is trapped until it does.

```tsx
import { useState } from "react";
import { Dialog, Button, Field, Input, Select, VStack } from "@protocore/pds";

export default function DialogComposition() {
  const [name, setName] = useState("");
  const [scope, setScope] = useState("read");

  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <Button>New API key</Button>
      </Dialog.Trigger>
      <Dialog.Content style={{ width: 460 }}>
        <Dialog.Title>Mint API key</Dialog.Title>
        <Dialog.Description>
          Keys are shown once at creation. Store it in your secrets manager.
        </Dialog.Description>

        <VStack gap={4} style={{ marginTop: 20 }}>
          <Field label="Label" hint="How you'll recognize this key later">
            <Input
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="ci-deploy-bot"
            />
          </Field>
          <Field label="Scope">
            <Select.Root value={scope} onValueChange={setScope}>
              <Select.Trigger>
                <Select.Value />
              </Select.Trigger>
              <Select.Content>
                <Select.Item value="read">Read only</Select.Item>
                <Select.Item value="write">Read + write</Select.Item>
                <Select.Item value="admin">Admin</Select.Item>
              </Select.Content>
            </Select.Root>
          </Field>
        </VStack>

        <Dialog.Footer>
          <Dialog.Close asChild>
            <Button variant="secondary">Cancel</Button>
          </Dialog.Close>
          <Dialog.Close asChild>
            <Button disabled={!name}>Generate</Button>
          </Dialog.Close>
        </Dialog.Footer>
      </Dialog.Content>
    </Dialog.Root>
  );
}
```

## Do & don't

**Do**

- Always render a Dialog.Title — Radix warns without one and screen readers need it.
- Wrap footer actions in Dialog.Close so the panel dismisses on click.
- Put the primary action last (rightmost) in the footer, secondary before it.
- Reach for ConfirmDialog when the task is a yes/no destructive confirmation.

**Don't**

- Nest a Dialog inside another Dialog — redesign the flow instead.
- Use a Dialog for an irreversible action's confirmation — use AlertDialog.
- Cram a whole workflow in; if it scrolls the viewport, use a Sheet or a page.
- Disable Esc / backdrop dismissal on a routine dialog — that's AlertDialog behavior.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Esc` | Closes the dialog and returns focus to the trigger. |
| `Tab` | Cycles focus forward, trapped within the panel. |
| `Shift + Tab` | Cycles focus backward, trapped within the panel. |
| `Enter / Space` | Activates the focused control (e.g. a footer button). |

**Notes**

- Focus is trapped inside the panel while open and restored to the trigger on close.
- Opening the dialog moves focus into the panel; the close × is reachable and labelled.
- The panel is role=dialog with aria-modal; Title labels it and Description describes it.
- Content outside the dialog is inert and hidden from assistive tech while it's open.

## Related

`alert-dialog`, `sheet`, `popover`, `command-palette`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
