# Dialog

An accessible React dialog for Next.js and Tailwind CSS: a modal window that grows out of its trigger, keeps focus inside and returns it, built on Radix Dialog.

Source: https://ui.fewtone.com/components/dialog

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/dialog.json
```

## Usage

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function InviteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Invite people</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Invite people</DialogTitle>
          <DialogDescription>They'll get an email with a link to join.</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button>Send invites</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

## API reference

### Dialog

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – | Whether it's open, when you control it. |
| `defaultOpen` | `boolean` | `false` | Whether it starts open. |
| `onOpenChange` | `(open: boolean) => void` | – | Called when it opens or closes: the trigger, a close button, Escape or the backdrop. |
| `modal` | `boolean` | `true` | Modal blocks the page behind, hides it from screen readers and keeps focus inside. Set false to let people use the page while it's open (there's no backdrop then). |

### DialogTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Use your own element, such as a Button, as the trigger. |

### DialogContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showCloseButton` | `boolean` | `true` | Shows a close button in the top corner, named Close. |
| `onOpenAutoFocus` | `(event: Event) => void` | – | Runs as focus moves in (to the first focusable element). Call event.preventDefault() to focus something else yourself. |
| `onCloseAutoFocus` | `(event: Event) => void` | – | Runs as focus goes back to the trigger. Call event.preventDefault() to send it elsewhere. |
| `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | – | Runs when Escape is pressed. Call event.preventDefault() to keep it open. |
| `onInteractOutside` | `(event: Event) => void` | – | Runs on a click or focus outside the window. Call event.preventDefault() to keep it open. |

### DialogHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### DialogFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### DialogTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### DialogDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### DialogClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Use your own element as the close button. |

### DialogPortal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `HTMLElement` | `document.body` | Where to render instead. |

### DialogOverlay

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

**When to use it:** Use Dialog for a focused task that interrupts the page, such as a short form or details that need a response. Use Alert Dialog to confirm a destructive action, and Sheet for longer panels beside the page.
**Built on:** Radix Dialog.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
**npm packages (installed by the CLI):** lucide-react@^1.52.0, motion@^14.0.0, radix-ui@^1.7.0.
**Installs these FewTone UI components with it:** `button`.

## Examples

### Edit profile

A form in a dialog. The window grows out of the button that opened it and shrinks back into it. Saving closes it, so it's controlled with open and onOpenChange.

```tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useId, useState } from "react";

/*
 * A form in a dialog. Saving (the button, or Enter in a field) closes it, so it's controlled; Cancel, the
 * close button, Escape and the backdrop close it too.
 */
export default function DialogEditProfile() {
  const id = useId();
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button variant="outline">Edit profile</Button>
      </DialogTrigger>
      <DialogContent className="max-w-md">
        <form
          className="grid gap-6"
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>Change how others see you. Save when you're done.</DialogDescription>
          </DialogHeader>
          <div className="grid gap-4">
            <div className="grid gap-2">
              <label htmlFor={`${id}-name`} className="text-sm font-medium">
                Name
              </label>
              <Input id={`${id}-name`} defaultValue="Maya Chen" autoComplete="name" />
            </div>
            <div className="grid gap-2">
              <label htmlFor={`${id}-username`} className="text-sm font-medium">
                Username
              </label>
              <Input id={`${id}-username`} defaultValue="maya" autoComplete="username" />
            </div>
          </div>
          <DialogFooter>
            <DialogClose asChild>
              <Button variant="outline">Cancel</Button>
            </DialogClose>
            <Button type="submit">Save changes</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}
```

### Long content

Long content scrolls between a header and a footer that stay in place. The window never grows past the screen; it keeps 16px clear on every side.

```tsx
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";

const terms = [
  {
    title: "Your account",
    body: "You're responsible for what happens under your account. Keep your password safe and tell us if someone else gets in.",
  },
  {
    title: "Your content",
    body: "What you make stays yours. You give us permission to store and show it only so the service can work.",
  },
  {
    title: "Fair use",
    body: "Don't overload the service, try to break it, or use it to send spam. We may slow down or pause accounts that do.",
  },
  {
    title: "Payments",
    body: "Paid plans renew each month until you cancel. Cancelling stops the next charge; the current month stays paid.",
  },
  {
    title: "Privacy",
    body: "We collect only what the service needs, and never sell it. The privacy policy lists what we keep and for how long.",
  },
  {
    title: "Changes",
    body: "If these terms change in a way that matters, we'll tell you by email at least 30 days before they apply.",
  },
  {
    title: "Ending",
    body: "You can close your account at any time. We'll delete your content within 30 days, unless the law says we must keep it.",
  },
];

/*
 * Long content scrolls inside the dialog, between a header and a footer that stay in place. The scrolling
 * area takes focus, so the arrow keys scroll it.
 */
export default function DialogScrolling() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Read the terms</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Terms of service</DialogTitle>
          <DialogDescription>Please read these before you continue.</DialogDescription>
        </DialogHeader>
        <section
          // biome-ignore lint/a11y/noNoninteractiveTabindex: a scrolling area must be reachable from the keyboard
          tabIndex={0}
          aria-label="Terms"
          className="grid max-h-72 gap-4 overflow-y-auto rounded-md text-sm outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)"
        >
          {terms.map((term, index) => (
            <div key={term.title} className="grid gap-1">
              <h3 className="font-medium">
                {index + 1}. {term.title}
              </h3>
              <p className="text-muted-foreground">{term.body}</p>
            </div>
          ))}
        </section>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Decline</Button>
          </DialogClose>
          <DialogClose asChild>
            <Button>Accept</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
```

## Related

- [Alert Dialog](https://ui.fewtone.com/components/alert-dialog.md): An accessible React alert dialog for Next.js that asks people to confirm destructive actions, with focus starting on Cancel, built on Radix Alert Dialog.
- [Sheet](https://ui.fewtone.com/components/sheet.md): An accessible React sheet for Next.js: a side panel that slides in from any edge of the screen, for forms, filters and navigation, built on Radix Dialog.
