<!-- Generated by emit-docs.ts — do not edit. -->
# Dialog

Modal dialog on the native <dialog> top layer: focus trap, aria-modal and focus restore come from the platform; title/footer slots, Esc/backdrop/ close-button dismissal via onClose(reason) (D50). `placement` moves the panel and nothing else (D66): an `inline-start` / `inline-end` Dialog is a drawer, pinned full-height to that edge, with the same modality, focus trap, focus restore and dismissal reasons as a centered one. There is deliberately no separate Drawer component — a sibling would have to duplicate or wrap all of that.

## Props

| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
| `children` | `ReactNode` | — | no | Body content, between the title and footer slots. |
| `open` | `boolean` | — | yes | Controlled open state; syncs to showModal()/close(). |
| `onClose` | `(reason: "esc" \| "backdrop" \| "close-button") => void` | — | yes | Called on every dismissal attempt with its source; the consumer flips `open`. |
| `title` | `ReactNode` | — | no | Heading; renders an <h2> wired to aria-labelledby. Without it, pass aria-label. |
| `footer` | `ReactNode` | — | no | Action row (Buttons — one accent per group, danger for destructive). |
| `width` | `400 \| 560 \| 720` | 560 | no | Panel width in px (400 \| 560 \| 720). For an `inline-*` placement this is the drawer's width; the height is always the full viewport. |
| `placement` | `"center" \| "inline-start" \| "inline-end"` | center | no | Where the panel sits. `inline-start` / `inline-end` pin it full-height to that edge, making it a drawer (D66). |
| `dismissible` | `boolean` | true | no | false = no close button, Esc and backdrop swallowed — footer is the only exit. |
| `ref` | `Ref<HTMLDialogElement>` | — | no | Forwarded ref to the underlying <dialog> element. |
| `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |

## Keyboard & assistive tech

| Keys | Behavior |
|---|---|
| Esc | Dismisses via onClose('esc') when dismissible; swallowed otherwise. |
| Tab | Focus is trapped inside by the native <dialog> top layer; restored on close. |

Rendered with showModal(): aria-modal, inert background and focus restore come from the platform. title wires aria-labelledby; without title, pass aria-label. Backdrop click dismisses only when dismissible. placement="inline-start"/"inline-end" pins the panel full-height to that edge — that is Psi's drawer, and it changes nothing about modality, the focus trap, focus restore or the dismissal reasons (D66). A drawer's panel scrolls internally so a dismissible={false} footer stays reachable.

## Theming

Override `--psi-dialog-*` custom properties at any scope.



## Rules

- One accent per visual group; everything else neutral or ghost.
- danger only for actions with real consequences.
- Sizes are px numbers (24|32|40|48), never S/M/L.
- Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.
- Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
- --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
- Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.
- Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination ("Rows per page" visible, so Field).
- Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
