# Dialog

> Modal dialog with focus trap, scroll lock, restore-on-close, and compound header/body/footer API.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://sisyphosui.com/docs/components/dialog
- WAI-ARIA pattern: [Dialog (Modal)](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { Dialog } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

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

export function ConfirmDelete() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)} color="error">Delete</Button>
      <Dialog open={open} onOpenChange={setOpen} size="md">
        <Dialog.Header>
          <Dialog.Title>Delete this item?</Dialog.Title>
          <Dialog.Close />
        </Dialog.Header>
        <Dialog.Body>
          <Dialog.Description>This action cannot be undone.</Dialog.Description>
        </Dialog.Body>
        <Dialog.Footer>
          <Button variant="outlined" onClick={() => setOpen(false)}>Cancel</Button>
          <Button color="error" onClick={() => setOpen(false)}>Delete</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### Vue 3+

```vue
<script setup lang="ts">
import { ref } from "vue";
import {
  Button,
  Dialog,
  DialogHeader,
  DialogTitle,
  DialogClose,
  DialogBody,
  DialogDescription,
  DialogFooter,
} from "@sisyphos-ui/vue";

const open = ref(false);
</script>

<template>
  <Button color="error" @click="open = true">Delete</Button>
  <Dialog v-model:open="open" size="md">
    <DialogHeader>
      <DialogTitle>Delete this item?</DialogTitle>
      <DialogClose />
    </DialogHeader>
    <DialogBody>
      <DialogDescription>This action cannot be undone.</DialogDescription>
    </DialogBody>
    <DialogFooter>
      <Button variant="outlined" @click="open = false">Cancel</Button>
      <Button color="error" @click="open = false">Delete</Button>
    </DialogFooter>
  </Dialog>
</template>
```

### Angular 17+

```ts
import { Component, signal } from "@angular/core";
import {
  Button,
  Dialog,
  DialogHeader,
  DialogTitle,
  DialogClose,
  DialogBody,
  DialogDescription,
  DialogFooter,
} from "@sisyphos-ui/angular";

@Component({
  selector: "app-confirm-delete",
  standalone: true,
  imports: [
    Button, Dialog, DialogHeader, DialogTitle, DialogClose,
    DialogBody, DialogDescription, DialogFooter,
  ],
  template: `
    <sui-button color="error" (buttonClick)="open.set(true)">Delete</sui-button>
    <sui-dialog [open]="open()" (openChange)="open.set($event)" size="md">
      <sui-dialog-header>
        <sui-dialog-title>Delete this item?</sui-dialog-title>
        <sui-dialog-close />
      </sui-dialog-header>
      <sui-dialog-body>
        <sui-dialog-description>This action cannot be undone.</sui-dialog-description>
      </sui-dialog-body>
      <sui-dialog-footer>
        <sui-button variant="outlined" (buttonClick)="open.set(false)">Cancel</sui-button>
        <sui-button color="error" (buttonClick)="open.set(false)">Delete</sui-button>
      </sui-dialog-footer>
    </sui-dialog>
  `,
})
export class ConfirmDeleteComponent {
  open = signal(false);
}
```

## Examples

### Default

Focus is trapped inside the dialog and returned to the trigger on close. Body scroll locks while open.

```tsx
import { useState } from "react";
import { Button, Dialog, Input } from "@sisyphos-ui/react";

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)}>Invite teammate</Button>
      <Dialog open={open} onOpenChange={setOpen} size="sm">
        <Dialog.Header>
          <Dialog.Title>Invite teammate</Dialog.Title>
          <Dialog.Description>
            Send an invite to join your workspace. They'll get an email with a magic link.
          </Dialog.Description>
        </Dialog.Header>
        <Dialog.Close />
        <Dialog.Body>
          <Input
            label="Work email"
            placeholder="teammate@company.com"
            type="email"
            fullWidth
          />
        </Dialog.Body>
        <Dialog.Footer>
          <Button variant="text" onClick={() => setOpen(false)}>Cancel</Button>
          <Button onClick={() => setOpen(false)}>Send invite</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### Sizes

`size` caps `max-width`. Use `full` to fill the viewport.

```tsx
import { useState } from "react";
import { Button, Dialog, type DialogSize } from "@sisyphos-ui/react";

const SIZES: DialogSize[] = ["sm", "md", "lg", "xl", "full"];

export function Example() {
  const [size, setSize] = useState<DialogSize | null>(null);

  return (
    <>
      {SIZES.map((s) => (
        <Button key={s} variant="outlined" onClick={() => setSize(s)}>
          {s}
        </Button>
      ))}
      <Dialog
        open={size !== null}
        onOpenChange={() => setSize(null)}
        size={size ?? "md"}
      >
        <Dialog.Header>
          <Dialog.Title>size = "{size}"</Dialog.Title>
          <Dialog.Description>The size prop caps max-width.</Dialog.Description>
        </Dialog.Header>
        <Dialog.Close />
        <Dialog.Body>Scrollable content lives inside Dialog.Body.</Dialog.Body>
        <Dialog.Footer>
          <Button onClick={() => setSize(null)}>Close</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### With form & initialFocus

`initialFocus` accepts a ref — the dialog focuses that element on open so keyboard users start on the first field.

```tsx
import { useRef, useState } from "react";
import { Button, Dialog, Input } from "@sisyphos-ui/react";

export function Example() {
  const [open, setOpen] = useState(false);
  const firstFieldRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <Button onClick={() => setOpen(true)}>Edit profile</Button>
      <Dialog
        open={open}
        onOpenChange={setOpen}
        size="md"
        initialFocus={firstFieldRef}
      >
        <Dialog.Header>
          <Dialog.Title>Edit profile</Dialog.Title>
          <Dialog.Description>Esc or backdrop click cancels.</Dialog.Description>
        </Dialog.Header>
        <Dialog.Close />
        <Dialog.Body>
          <form id="profile-form" onSubmit={(e) => { e.preventDefault(); setOpen(false); }}>
            <Input ref={firstFieldRef} label="Display name" defaultValue="Volkan Günay" fullWidth />
            <Input label="Email" type="email" defaultValue="me@example.com" fullWidth />
          </form>
        </Dialog.Body>
        <Dialog.Footer>
          <Button variant="text" onClick={() => setOpen(false)}>Cancel</Button>
          <Button form="profile-form" type="submit">Save changes</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### Destructive confirm

Pair a dialog with a destructive action and a cautious copy — default focus stays on Cancel.

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

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button color="error" variant="outlined" onClick={() => setOpen(true)}>
        Delete workspace
      </Button>
      <Dialog open={open} onOpenChange={setOpen} size="sm">
        <Dialog.Header>
          <Dialog.Title>Delete "Acme"?</Dialog.Title>
          <Dialog.Description>
            This permanently deletes the workspace and all of its data.
          </Dialog.Description>
        </Dialog.Header>
        <Dialog.Close />
        <Dialog.Footer>
          <Button variant="text" onClick={() => setOpen(false)}>Cancel</Button>
          <Button color="error" onClick={() => setOpen(false)}>Yes, delete</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### Auto-rendered close button

Set `showCloseButton` to get a close affordance in the top-right without composing `<Dialog.Close />` manually. `closeButtonLabel` overrides the aria-label.

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

export function Example() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Open dialog</Button>
      <Dialog open={open} onOpenChange={setOpen} showCloseButton size="sm">
        <Dialog.Header>
          <Dialog.Title>Quick note</Dialog.Title>
          <Dialog.Description>
            No manual Dialog.Close composition needed for the common case.
          </Dialog.Description>
        </Dialog.Header>
        <Dialog.Footer>
          <Button onClick={() => setOpen(false)}>Got it</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

### No backdrop

Set `backdrop={false}` + `closeOnBackdropClick={false}` for contextual, in-place sheets.

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

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="outlined" onClick={() => setOpen(true)}>
        Show a floating sheet
      </Button>
      <Dialog
        open={open}
        onOpenChange={setOpen}
        backdrop={false}
        closeOnBackdropClick={false}
        size="sm"
      >
        <Dialog.Header>
          <Dialog.Title>Keyboard tips</Dialog.Title>
          <Dialog.Description>Transparent overlay — use for contextual sheets.</Dialog.Description>
        </Dialog.Header>
        <Dialog.Close />
        <Dialog.Footer>
          <Button onClick={() => setOpen(false)}>Got it</Button>
        </Dialog.Footer>
      </Dialog>
    </>
  );
}
```

## Anatomy

```tsx
<Dialog.Root>
  <Dialog.Trigger /> // Button that opens the dialog.
  <Dialog.Overlay /> // Scrim behind the dialog; closes on click.
  <Dialog.Content /> // Focus-trapped panel. (required)
  <Dialog.Header /> // Optional titled header slot.
  <Dialog.Body /> // Scrollable content region.
  <Dialog.Footer /> // Action buttons row.
  <Dialog.Close /> // Closes the dialog and restores focus.
</Dialog.Root>
```

## Keyboard interactions

- **Tab** — Moves focus to the next focusable element within the dialog. Focus is trapped.
- **Shift+Tab** — Moves focus to the previous focusable element.
- **Esc** — Closes the dialog and returns focus to the trigger.

<!-- exports: { "Dialog": "@sisyphos-ui/react" } -->