# Dialog

> A window over the page, with a backdrop, for a choice or a short task that must be done or cancelled before going on.

- Name: `dialog`
- Status: beta
- Categories: overlays
- File: the install writes `dialog.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Other registry items: `@txbrendan/action-keys`. The install adds them.
- Gallery: https://txbrendan.dev/components/dialog
- Registry item: https://txbrendan.dev/r/dialog.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the component:

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

## Use it for

- A choice people must make before they go on, like confirming a delete.
- A short form that needs full attention, like editing a profile.
- Always give it a DialogTitle. It names the dialog for screen readers.
- Put the actions in DialogFooter, with the safe choice before the main action, which goes last.
- An extra quiet action, like Discard, goes first as a Button with emphasis="ghost". It sits at the start of the row, away from the main action.
- The dialog opens at the end of the page body. In a box with its own theme, like a dark preview on a light page, pass that box as container on DialogPopup so the dialog and its backdrop keep the box's colors.

## Avoid it for

- Extra details or a quick edit beside a button. Use a popover, which does not block the page.
- A message that needs no answer. Use an alert on the page.
- Long forms or many steps. Use a page.
- Opening a dialog when the page loads. People did not ask for it.
- More than three actions, or long labels, in DialogFooter. It stays on one row, so on a phone the dialog would scroll sideways.
- closeButton={false} when the footer starts with Discard or Delete and no field comes before it. Focus would start on that button, so Enter would run it.

## Props

- `Dialog`: `{ open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, details) => void; disablePointerDismissal?: boolean }`. Root. Holds the open state and renders no element. Takes open, defaultOpen, onOpenChange, and disablePointerDismissal.
- `open`: `boolean`. Dialog. Whether it is open, controlled. Use it with onOpenChange.
- `defaultOpen`: `boolean`. Default `false`. Dialog. Whether it is open at the start, when you do not control it.
- `onOpenChange`: `(open: boolean, details) => void`. Dialog. Called with the new open state when it opens or closes.
- `disablePointerDismissal`: `boolean`. Default `false`. Dialog. Keeps a press outside from closing it. Esc and the Close button still close it.
- `DialogTrigger`: `ComponentProps<"button"> & { render?: ReactElement }`. Part. The button that opens it. Pass render={\<Button />} to use your own button.
- `DialogPopup`: `ComponentProps<"div">`. Part. The dialog, centered over a backdrop. It renders in a portal while open.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. DialogPopup. Most width: 24rem, 32rem, or 42rem. It never gets wider than the screen.
- `closeButton`: `boolean`. Default `true`. DialogPopup. Shows a Close button with an X in the top corner. Turn it off only when the footer has a Cancel button.
- `DialogTitle`: `ComponentProps<"h2">`. Part. The dialog's name. An \<h2> by default.
- `DialogDescription`: `ComponentProps<"p">`. Part. Muted text under the title. Screen readers read it when the dialog opens.
- `DialogFooter`: `Omit<ComponentProps<"div">, "color">`. Part. One row of actions at the bottom. Ghost buttons sit at the start, and the rest line up at the end.
- `DialogClose`: `ComponentProps<"button"> & { render?: ReactElement }`. Part. A button that closes the dialog, like Cancel.
- `initialFocus`: `RefObject<HTMLElement | null> | boolean`. DialogPopup. Where focus goes when it opens. By default, the Close button, or the first control inside when there is none.
- `container`: `HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null`. DialogPopup. The element the dialog and its backdrop open in. By default, the end of the page body, so no parent can clip them. Pass an element, or a ref itself (not ref.current), to open them inside, for example to keep a parent's theme. Only client code has elements and refs. A null container shows no dialog, but still locks page scroll until it closes.
- `effect`: `"rise" | "grow"`. Default `"rise"`. DialogPopup. How the dialog and its backdrop open and close. rise fades in and rises a little. grow fades in and grows in place, and closes the same way. Left out, it follows an effect-\<name> class on \<body>, where popups open, and rises with none.
- `className`: `string`. DialogPopup. Extra classes. Later classes win, so className='max-w-xl' replaces the width.

## Accessibility

- The popup has role="dialog". DialogTitle names it, and DialogDescription describes it.
- While it is open, the rest of the page is hidden from screen readers with aria-hidden.
- Opening it moves focus to the Close button in the corner, which is first in the tab order. With closeButton={false}, focus goes to the first control inside.
- Tab stays inside until it closes, even when pressed fast. Then focus goes back to the trigger.
- Escape closes it, and so does a click on the backdrop. Set disablePointerDismissal on Dialog to ignore backdrop clicks.
- The page behind cannot be scrolled or reached while it is open.
- The Close button in the corner is named Close. Touch screen reader users cannot press Escape, so keep it, or keep a Cancel button.
- The theme tests hold dialog text and muted text to 4.5:1 on the popover color, and focus rings and input borders to 3:1, in light and dark.

## Examples

### Confirm a delete (needs @txbrendan/button)

```tsx
"use client";

import { Button } from "@/components/ui/button";

function DeleteProject({ onDelete }: { onDelete: () => void }) {
  return (
    <Dialog>
      <DialogTrigger render={<Button tone="danger" emphasis="soft" />}>Delete project</DialogTrigger>
      <DialogPopup size="sm" closeButton={false}>
        <DialogTitle>Delete this project?</DialogTitle>
        <DialogDescription>This deletes every file in it. You cannot undo it.</DialogDescription>
        <DialogFooter>
          <DialogClose render={<Button emphasis="outline" />}>Cancel</DialogClose>
          <DialogClose render={<Button tone="danger" />} onClick={onDelete}>
            Delete project
          </DialogClose>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  );
}
```

### A form that closes when it saves (needs @txbrendan/button, input, and label)

```tsx
"use client";

import { useState } from "react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

function EditProfile({ name, save }: { name: string; save: (data: FormData) => Promise<void> }) {
  const [open, setOpen] = useState(false);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button emphasis="outline" />}>Edit profile</DialogTrigger>
      <DialogPopup>
        <DialogTitle>Edit profile</DialogTitle>
        <DialogDescription>Your name shows on every comment you leave.</DialogDescription>
        <form
          action={async (data) => {
            await save(data);
            setOpen(false);
          }}
        >
          <div className="mt-6 flex flex-col gap-1.5">
            <Label htmlFor="profile-name">Name</Label>
            <Input id="profile-name" name="name" defaultValue={name} />
          </div>
          <DialogFooter>
            <DialogClose render={<Button emphasis="outline" />}>Cancel</DialogClose>
            <Button type="submit" tone="brand">
              Save
            </Button>
          </DialogFooter>
        </form>
      </DialogPopup>
    </Dialog>
  );
}
```

### Wider, for a table or a long form (needs @txbrendan/button)

```tsx
import { Button } from "@/components/ui/button";

<Dialog>
  <DialogTrigger render={<Button emphasis="outline" />}>Import contacts</DialogTrigger>
  <DialogPopup size="lg">
    <DialogTitle>Import contacts</DialogTitle>
    <DialogDescription>Check each row before you import it.</DialogDescription>
  </DialogPopup>
</Dialog>
```

### Grows in place (needs @txbrendan/button)

```tsx
import { Button } from "@/components/ui/button";

<Dialog>
  <DialogTrigger render={<Button emphasis="outline" />}>Rename</DialogTrigger>
  <DialogPopup effect="grow">
    <DialogTitle>Rename file</DialogTitle>
    <DialogDescription>The new name shows for everyone with access.</DialogDescription>
  </DialogPopup>
</Dialog>
```
