# Dialog

A modal surface: portal, scrim, centred panel.

- Group: Overlays
- Import: `import { Dialog } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/dialog/
- Related: dropdown-menu, tooltip

## Anatomy

- **Scrim** (required) — The full-viewport --scrim layer at --z-overlay (200). It is what a click outside lands on, and it paints over everything the page had pinned below that rank — a FloatingIconButton at 100 included. Not over an anchored panel: those sit at 220 precisely so a Select opened inside this dialog is still reachable.
- **Panel** (required) — The centred box at --z-modal (210), capped at min(92vw, 32rem) wide and 85vh tall, scrolling its own body past that. It centres itself with a translate, which has consequences for anything fixed inside it.
- **Title** (required) — title, rendered as the Radix Title. Always present: when title is omitted a visually hidden one is rendered reading the literal word “Dialog”, and development warns DIALOG_TITLE_MISSING — the fallback exists so an unnamed modal is not shipped, not so one can be.
- **Description** — description, a quiet line under the title. It shares one wrapper with the title, so hideTitle hides both.
- **Close** — The 36px X in the top-end corner, rendered while showClose is true (the default) and carrying its own aria-label of “Close”.

## Best practices

### Do

- Pass title even when you set hideTitle: with no title at all the fallback accessible name is the literal string “Dialog”, so every unnamed modal in the app is announced as the same thing — and it passes an automated accessibility check while doing it, which is why development warns instead of leaving it to a review.
- Wrap the cancelling control in DialogClose rather than flipping your own state — the close then runs through Radix, which returns focus to the trigger instead of dropping it at the top of the document.
- Keep it to what fits. The panel stops at 32rem by 85vh and scrolls its own body past that, so a form long enough to scroll has become a Sheet, which gets the full height of the viewport, or a page.
- Leave showClose on unless the panel supplies its own exit: Escape and the scrim are the only other ways out and neither is visible, so showClose={false} on a dialog full of content is a room with an unmarked door.

### Don’t

- An OverlayContainer whose element is not positioned hands the dialog the wrong box: naming a container switches the panel from fixed to absolute, and an unpositioned container sends it to the nearest positioned ancestor instead — usually the page, which looks like the container was ignored.
- Two dialogs open at once are ordered by the DOM, not by a rank: both sit at --z-modal, so the one mounted last paints over the first — a command palette summoned over a dialog lands on top because it opened second, and reversing that order reverses the picture.
- The panel centres itself with a transform, which makes it the containing block for every position: fixed descendant — a FloatingIconButton dropped inside a dialog pins to the panel’s corner rather than the screen’s.

## Accessibility

- Radix owns the focus trap, Escape, the scroll lock and aria-modal.
- A dialog without a visible heading still renders a hidden title, rather than shipping an unnamed modal.

## Keyboard

- Escape — Closes it, and focus returns to the trigger it came from.
- Tab — Cycles inside the dialog; focus cannot leave while it is open.

## Dialog

Re-export of `DialogPrimitive.Root`.

Radix Dialog root + trigger + close, re-exported as typed passthroughs.

## DialogTrigger

Re-export of `DialogPrimitive.Trigger`.

## DialogClose

Re-export of `DialogPrimitive.Close`.

## DialogContent

A modal surface: portal → scrim → centred panel. Radix owns the focus trap, the escape key, the scroll lock and the `aria-modal` wiring — all of which a hand-rolled dialog gets subtly wrong, usually by leaving focus behind in the page underneath. Radix requires a `Dialog.Title` whether or not one is shown, so a dialog without a visible heading still renders a hidden one rather than shipping an unnamed modal — and warns in development, because the fallback it renders is the literal word "Dialog" and a placeholder that passes an accessibility check is how the problem survives a review. Portals into the element an enclosing `OverlayContainer` names, and switches from viewport positioning to container positioning when there is one. A `fixed` panel covers the page whatever it is portalled into, so honouring the container without that swap would have moved the markup and left the picture unchanged.

### Props

- `title` — `ReactNode`. Heading text. Pass one even when `hideTitle` is set. Radix requires a title, so omitting it renders a hidden fallback reading the literal word "Dialog" — which satisfies an automated accessibility check and announces every unnamed modal in the application as the same thing. Development says so out loud.
- `description` — `ReactNode`. Sub-heading under the title.
- `className` — `string`.
- `showClose` — `boolean` default `true`. Show the top-right close control (default true).
- `hideTitle` — `boolean` default `false`. Keeps the title for assistive tech and hides it visually. For a surface whose purpose is obvious to anyone who can see it — a command palette, a media lightbox — where a printed heading would be furniture. The title itself is never optional: Radix requires one, and a modal with no accessible name drops a screen reader into an unnamed region.

Also accepts: `Omit<ComponentProps<typeof DialogPrimitive.Content>, 'title' | 'className'>`.

## Example — a destructive confirm

```tsx
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from '@misoto22/folio'

<Dialog>
  <DialogTrigger asChild>
    <Button variant="danger">Delete frame</Button>
  </DialogTrigger>
  <DialogContent
    title="Delete this frame?"
    description="It will be removed from the archive and from every collection it appears in."
  >
    <div className="mt-6 flex justify-end gap-3">
      <DialogClose asChild>
        <Button variant="secondary">Cancel</Button>
      </DialogClose>
      <DialogClose asChild>
        <Button variant="danger">Delete</Button>
      </DialogClose>
    </div>
  </DialogContent>
</Dialog>
```

## Example — a hidden title

```tsx
<Dialog>
  <DialogTrigger asChild>
    <Button variant="secondary">Review the release</Button>
  </DialogTrigger>
  <DialogContent title="Release 0.4.0" hideTitle>
    <div className="flex items-center gap-3">
      <span className="eyebrow text-(--ink-3-aa)">changeset</span>
      <Badge tone="success">ready</Badge>
    </div>
    <Heading level={2} size="item" className="mt-2">
      Release 0.4.0
    </Heading>
    <Text size="sm" className="mt-3">
      Five packages, one minor bump. The tag is cut when this merges, and the
      registry has the tarball about ninety seconds later.
    </Text>
    <div className="mt-6 flex justify-end gap-3">
      <DialogClose asChild>
        <Button variant="secondary">Not yet</Button>
      </DialogClose>
      <DialogClose asChild>
        <Button>Merge and tag</Button>
      </DialogClose>
    </div>
  </DialogContent>
</Dialog>
```

## Example — or a sheet

```tsx
<div className="flex flex-wrap items-center gap-3">
  <Dialog>
    <DialogTrigger asChild>
      <Button variant="secondary">Rename (dialog)</Button>
    </DialogTrigger>
    <DialogContent title="Rename this collection" description="Its public link does not change.">
      <Field label="Name" className="mt-4">
        <Input defaultValue="Kyoto, February" />
      </Field>
      <div className="mt-6 flex justify-end gap-3">
        <DialogClose asChild>
          <Button variant="secondary">Cancel</Button>
        </DialogClose>
        <DialogClose asChild>
          <Button>Rename</Button>
        </DialogClose>
      </div>
    </DialogContent>
  </Dialog>

  <Sheet>
    <SheetTrigger asChild>
      <Button variant="secondary">Edit details (sheet)</Button>
    </SheetTrigger>
    <SheetContent
      side="end"
      title="Collection details"
      description="Six fields, and room to see them all at once."
    >
      <div className="mt-4 flex flex-col gap-4">
        <Field label="Name">
          <Input defaultValue="Kyoto, February" />
        </Field>
        <Field label="Slug" hint="Used in the public link.">
          <Input defaultValue="kyoto-february" />
        </Field>
        <Field label="Camera">
          <Input defaultValue="Pentax 67" />
        </Field>
        <Field label="Notes">
          <Textarea rows={4} defaultValue="Portra 400, pushed one stop." />
        </Field>
        <div className="flex justify-end gap-3">
          <SheetClose asChild>
            <Button variant="secondary">Cancel</Button>
          </SheetClose>
          <SheetClose asChild>
            <Button>Save</Button>
          </SheetClose>
        </div>
      </div>
    </SheetContent>
  </Sheet>
</div>
```
