# Dialog

> A focused surface for decisions that need attention.

- Category: Overlays
- Import: `import { Dialog } from "@sagui/ui";`
- Page: /components/dialog

```tsx
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@sagui/ui";

export function Hero() {
  return (
    <Dialog>
      <DialogTrigger asChild><Button variant="danger">Delete project</Button></DialogTrigger>
      <DialogContent title="Delete Atlas redesign?" description="This removes the project and its 14 files for everyone.">
        <p className="mb-5 text-muted-foreground">You can restore it from the trash for 30 days.</p>
        <div className="flex justify-end gap-2">
          <DialogClose asChild><Button variant="outline">Cancel</Button></DialogClose>
          <DialogClose asChild><Button variant="danger">Delete</Button></DialogClose>
        </div>
      </DialogContent>
    </Dialog>
  );
}
```

## When to use

- Confirmations and decisions that must interrupt, such as Delete project.
- Short forms like rename or invite that fit in one focused panel.
- Flows where the dialog title changes between steps and should crossfade in place.

## When not to use

- Use drawer for long forms or detail panels that keep the page in context.
- Use bottom-sheet for mobile-first secondary tasks with snap heights.
- Use popover for light, non-modal content anchored to a trigger.

## Installation

Install the package and import the styles once. The [installation guide](/docs/installation) covers the Tailwind setup.

```bash
npm install @sagui/ui
```

## Usage

```tsx
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@sagui/ui";
import { Button } from "@sagui/ui";

export function RenameProject() {
  return (
    <Dialog>
      <DialogTrigger asChild><Button>Rename</Button></DialogTrigger>
      <DialogContent title="Rename project" description="This changes the URL too.">
        <input defaultValue="Arc" aria-label="Project name" />
        <DialogClose asChild><Button>Save</Button></DialogClose>
      </DialogContent>
    </Dialog>
  );
}
```

## Examples

### Controlled

Pass `open` and `onOpenChange` to open it from anywhere in your code.

```tsx
import { useState } from "react";
import { Button, Dialog, DialogContent } from "@sagui/ui";

export function Controlled() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>Invite people</Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent title="Invite people" description="They will get an email with a link.">
          <div className="flex justify-end gap-2">
            <Button variant="outline" onClick={() => setOpen(false)}>Not now</Button>
            <Button onClick={() => setOpen(false)}>Send invites</Button>
          </div>
        </DialogContent>
      </Dialog>
    </>
  );
}
```

### Copy that changes while open

The title and description reword in place, so a multi-step flow never swaps the whole surface.

```tsx
import { useState } from "react";
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@sagui/ui";

export function ChangingCopy() {
  const [step, setStep] = useState(1);
  return (
    <Dialog>
      <DialogTrigger asChild><Button variant="outline">Two step flow</Button></DialogTrigger>
      <DialogContent title={step === 1 ? "Step 1: choose a plan" : "Step 2: confirm"} description={step === 1 ? "You can change it later." : "You will be billed today."}>
        <div className="flex justify-end gap-2">
          <Button variant="outline" onClick={() => setStep(step === 1 ? 2 : 1)}>{step === 1 ? "Next" : "Back"}</Button>
          <DialogClose asChild><Button>Done</Button></DialogClose>
        </div>
      </DialogContent>
    </Dialog>
  );
}
```

## API reference

### Dialog

Root that tracks open state so the content can animate out. Controlled or uncontrolled.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – | Controlled open state. |
| `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. |
| `onOpenChange` | `(open: boolean) => void` | – | Called when the dialog opens or closes. |
| `...props` | `ComponentPropsWithoutRef<typeof DialogPrimitive.Root>` | – | Other Radix Dialog root props, such as modal. |

### DialogTrigger

Radix Dialog.Trigger. Use asChild to wrap your own button.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `...props` | `ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>` | – | Radix trigger props, including asChild. |

### DialogContent

Portaled overlay and panel with a titled header and built-in close button.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `string` | – | Dialog title, rendered as Radix Dialog.Title. Changes crossfade while open. |
| `description` | `string` | – | Optional supporting line, rendered as Dialog.Description. |
| `children` (required) | `ReactNode` | – | Body content, usually a form or actions. |
| `...props` | `ComponentPropsWithoutRef<typeof DialogPrimitive.Content>` | – | Radix content props such as className, onEscapeKeyDown, and onPointerDownOutside. |

### DialogClose

Radix Dialog.Close for custom cancel or confirm buttons.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `...props` | `ComponentPropsWithoutRef<typeof DialogPrimitive.Close>` | – | Radix close props, including asChild. |

## Keyboard interactions

| Keys | Action |
| --- | --- |
| Escape | Closes the dialog and returns focus to the trigger. |
| Tab / Shift+Tab | Cycles focus within the dialog. |

## Accessibility

- Radix renders role="dialog" with aria-modal, traps focus, and restores it to the trigger on close.
- title and description are wired to aria-labelledby and aria-describedby.
- The close button carries aria-label="Close dialog".

## Motion

- The overlay fades while the panel rises 8px and scales from 0.96 on a smooth spring; closing is shorter and retargets from the current state.
- Title and description changes rise in with a soft blur.
- Reduced motion uses a plain opacity fade.

## Responsive behavior

- The panel is min(100vw minus a 32px gutter, 440px) wide and centered, so it fits phones without extra CSS.
- Height caps at the viewport minus a gutter and the panel scrolls beyond that.

## Performance

- The overlay uses a 7px backdrop blur, which can cost frames on low-end devices over busy pages.
- Content renders in a portal only while open and unmounts after the exit animation.

## Notes

- Use for decisions that must interrupt: confirmations, short forms. Use drawer for side panels, bottom-sheet for mobile-first secondary tasks, popover for light non-modal content.
- Always compose Dialog > DialogTrigger + DialogContent. Wrap your own buttons with asChild.

## Related

- [Drawer](/components/drawer): A temporary side surface for focused work.
- [Bottom sheet](/components/bottom-sheet): A sheet that rests at a peek or full height and follows your finger.
- [Popover](/components/popover): A small anchored surface for contextual information.
