> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dialog - Web (AI Kit)

> Dialogs present focused tasks or decisions in a modal overlay.

> [!NOTE]
>
> For development, import `Dialog` from
> `@servicetitan/anvil2-ai-kit`. For design, find it in the
> [AI Kit Web Component Library file](https://www.figma.com/design/QEIsArLktLeJAaFvcpw3PO/AI-Kit-Web-Component-Library).

ai-kit-dialog

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Leave without saving?"
      content="Your changes haven't been saved. Leaving now will discard them."
      primaryAction={{ label: "Leave" }}
      secondaryAction={{ label: "Stay" }}
      defaultOpen
      trigger={<Button label="Open dialog" />}
    />
  );
}

export default App;
```

## Options

The Dialog supports a primary action, an optional secondary action, and a danger variant for destructive confirmations.

### Actions

ai-kit-dialog-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-actions?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="You're all set"
      content="Your workspace is ready. You can invite teammates any time from Settings."
      primaryAction={{ label: "Got it" }}
      defaultOpen
      trigger={<Button label="Finish setup" />}
    />
  );
}

export default App;
```

Omit `secondaryAction` for a single-action, acknowledge-only dialog — the lone action sits at the trailing edge of the footer. Pass both for a confirm/cancel pair, as in the hero example above.

### Action Variants

#### Primary Action

ai-kit-dialog-primary-action-variants

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-primary-action-variants?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Save changes?"
      content="Save your changes before leaving this page."
      primaryAction={{ label: "Save" }}
      secondaryAction={{ label: "Discard" }}
      defaultOpen
      trigger={<Button label="Open dialog" />}
    />
  );
}

export default App;
```

`primaryAction` defaults to `variant: "primary"`, rendering the standard confirm treatment.

#### Danger Action

ai-kit-dialog-danger-action

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-danger-action?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Delete project?"
      content="This permanently deletes the project and everything in it. This can't be undone."
      primaryAction={{ label: "Delete", variant: "danger" }}
      secondaryAction={{ label: "Cancel" }}
      defaultOpen
      trigger={<Button label="Delete project" variant="danger" />}
    />
  );
}

export default App;
```

Set `primaryAction`'s `variant` to `"danger"` for a destructive confirmation — it renders the critical (red) Button so the risk of the action is visually distinct.

#### No Actions

ai-kit-dialog-no-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-no-actions?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="View attachment"
      content={
        <Text text="Image preview would appear here. The dialog renders no footer — only the header's X to close." />
      }
      defaultOpen
      trigger={<Button label="View attachment" variant="secondary" />}
    />
  );
}

export default App;
```

Omit both `primaryAction` and `secondaryAction` for a footerless dialog — useful for a media preview or informational dialog where the header's X close button is the only dismiss affordance.

## Behavior

The Dialog traps focus while open and scrolls its own body independently of the pinned header and footer.

### Scrolling Body

ai-kit-dialog-scrolling-body

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-scrolling-body?file=src/App.tsx)

```tsx lines theme={null}
import { Button, Dialog, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Terms of service"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {Array.from({ length: 12 }, (_, i) => (
            <Text
              key={i}
              text={`Section ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`}
            />
          ))}
        </div>
      }
      primaryAction={{ label: "Accept" }}
      secondaryAction={{ label: "Decline" }}
      defaultOpen
      trigger={<Button label="Review terms" />}
    />
  );
}

export default App;
```

Body content beyond 90% of the viewport height scrolls within the dialog while the header and footer stay pinned.

## Usage Guidelines

### When to Use

Use the Dialog when:

* **Confirming a decision before continuing** – The user must confirm or cancel, such as leaving a page with unsaved changes. Pair a primary action with a secondary action.
* **Confirming a destructive action** – The action deletes data or can't be undone. Use the danger primary action so the risk is visually distinct.
* **Acknowledging a message** – The user only needs to read the content and dismiss it. Use a single primary action.
* **Previewing media or information** – Content such as an attachment preview needs a focused view with no footer actions. The header close button dismisses it.

### When not to use

Avoid using the Dialog for:

* **Presenting forms, lists, or composed layouts** – Dialog content is message-style: text, phrasing content, or rendered Markdown. Use the [Bottom Sheet](https://anvil.servicetitan.com/docs/components/bottom-sheet/web.md) instead.
* **Asking a clarifying question before the agent continues** – Use the [Agent Prompt](https://anvil.servicetitan.com/docs/components/agent-prompt/web.md) instead, which asks inline in place of the Prompt Bar.

## Keyboard Interaction

Users can navigate, scroll, and dismiss the Dialog using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus among the close button, body content, and footer actions |
| Enter / Space | Activates the focused close button or footer action |
| Escape | Dismisses the dialog |

## Code

**Implementation**

ai-kit-dialog

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Leave without saving?"
      content="Your changes haven't been saved. Leaving now will discard them."
      primaryAction={{ label: "Leave" }}
      secondaryAction={{ label: "Stay" }}
      defaultOpen
      trigger={<Button label="Open dialog" />}
    />
  );
}

export default App;
```

## Common Examples

To use the `Dialog`, pass the required `title` and `content` props. Open it with a Carto `Button` through `trigger`, or control it with `isOpen` and `onOpenChange`.

```tsx theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function ExampleComponent() {
  return (
    <Dialog
      title="Leave without saving?"
      trigger={<Button label="Leave page" />}
      content="Your changes haven't been saved."
      primaryAction={{ label: "Leave" }}
      secondaryAction={{ label: "Stay" }}
    />
  );
}
```

### Actions

Pass `primaryAction` for the confirm action and `secondaryAction` for dismissal. The dialog fixes both buttons at medium size and fixes the secondary action to the secondary variant.

ai-kit-dialog-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-actions?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="You're all set"
      content="Your workspace is ready. You can invite teammates any time from Settings."
      primaryAction={{ label: "Got it" }}
      defaultOpen
      trigger={<Button label="Finish setup" />}
    />
  );
}

export default App;
```

Use `variant: "danger"` on `primaryAction` for destructive confirmation.

ai-kit-dialog-danger-action

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-danger-action?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Button, Dialog } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Delete project?"
      content="This permanently deletes the project and everything in it. This can't be undone."
      primaryAction={{ label: "Delete", variant: "danger" }}
      secondaryAction={{ label: "Cancel" }}
      defaultOpen
      trigger={<Button label="Delete project" variant="danger" />}
    />
  );
}

export default App;
```

### Footerless

Omit both actions for a footerless dialog. The header close button remains available.

ai-kit-dialog-no-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-no-actions?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Button, Dialog, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="View attachment"
      content={
        <Text text="Image preview would appear here. The dialog renders no footer — only the header's X to close." />
      }
      defaultOpen
      trigger={<Button label="View attachment" variant="secondary" />}
    />
  );
}

export default App;
```

### Controlled state

Omit `trigger` and pass `isOpen` with `onOpenChange` when an event or another component opens the dialog. Footer actions request closure after their own `onPress` handlers run. For asynchronous flows that must remain open, treat `onOpenChange` as a request and retain control of `isOpen`.

```tsx theme={null}
<Dialog
  title="Rename"
  isOpen={isOpen}
  onOpenChange={setOpen}
  content={<Text text="Choose a new name." />}
  primaryAction={{ label: "Save", onPress: save }}
  secondaryAction={{ label: "Cancel" }}
/>
```

### Scrolling body

Content beyond `90vh` scrolls inside the body while the header and footer remain visible.

ai-kit-dialog-scrolling-body

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-dialog-scrolling-body?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Button, Dialog, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <Dialog
      title="Terms of service"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {Array.from({ length: 12 }, (_, i) => (
            <Text
              key={i}
              text={`Section ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`}
            />
          ))}
        </div>
      }
      primaryAction={{ label: "Accept" }}
      secondaryAction={{ label: "Decline" }}
      defaultOpen
      trigger={<Button label="Review terms" />}
    />
  );
}

export default App;
```

## Internationalization

Localize `title`, `content`, and action labels before passing them to `Dialog`.

> [!NOTE]
>
> The built-in header close button currently uses the English accessible
> label `"Close"`.

## React Accessibility

* The required `title` labels the modal through `aria-labelledby`.
* React Aria supplies focus trapping and restoration, scroll lock, `aria-modal`, Escape dismissal, and outside-interaction dismissal.
* The body is always keyboard-focusable so keyboard users can scroll plain text.
* Set `isKeyboardDismissDisabled` only when the flow requires blocking Escape.

**Dialog Props**

```tsx theme={null}
<Dialog
  title="Delete project?"
  content="This action cannot be undone."
  trigger={<Button label="Delete project" variant="danger" />}
  primaryAction={{ label: "Delete", variant: "danger" }}
  secondaryAction={{ label: "Cancel" }}
/>
```

## `Dialog` Props

The `Dialog` accepts the following props and forwards remaining React Aria `ModalOverlay` state and dismissal props.

**content** `ReactNode` *required*

Message-style body content. Pass text, phrasing content, or rendered
`Markdown`, not arbitrary page layout.

**title** `string` *required*

Accessible title rendered in the header.

**className** `string`

Class merged onto the dialog surface.

**defaultOpen** `boolean`

Whether the dialog starts open when uncontrolled.

**isDismissable** `boolean` (default: true)

Whether outside interaction closes the dialog.

**isKeyboardDismissDisabled** `boolean`

Prevents Escape from closing the dialog.

**isOpen** `boolean`

Controlled open state. Pair with `onOpenChange`.

**onOpenChange** `(isOpen: boolean) =\> void`

Called when the open state changes.

**primaryAction** `DialogPrimaryAction`

Confirm action. Accepts `Button` props except `size`; supports
`variant="primary"` (default) or `variant="danger"`.

**secondaryAction** `Omit\<ButtonProps, 'size' | 'variant'\>`

Optional dismiss action. Always renders at medium size with the secondary
variant.

**trigger** `ReactElement\<ButtonProps\>`

Carto `Button` that opens the dialog. Omit for controlled state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.