@ -17,9 +17,7 @@ Three things make an `AlertDialog` different from a plain `Dialog`:
< AlertDialog.Overlay / >
< AlertDialog.Content >
< AlertDialog.Title > Delete account?< / AlertDialog.Title >
< AlertDialog.Description >
This is permanent and cannot be undone.
< / AlertDialog.Description >
< AlertDialog.Description > This is permanent and cannot be undone.< / AlertDialog.Description >
< AlertDialog.Cancel > Cancel< / AlertDialog.Cancel >
< AlertDialog.Action onclick = {deleteAccount} > Delete< / AlertDialog.Action >
< / AlertDialog.Content >
@ -32,37 +30,37 @@ By convention, place `Cancel` before `Action` in DOM order: the focus scope land
## Parts
| Part | Element | Origin | Description |
| ------------- | ---------- | ------------ | --------------------------------------------------------------------------- |
| `Provider` | none | AlertDialog | Root context. Forces `variant='alertdialog'` + `modal=true` on Dialog. |
| `Trigger` | `<button>` | Dialog | Opens the dialog on click. |
| `Content` | `<div>` | Dialog | Dialog container. Focus trap, scroll lock, dismissal layers integrated. |
| `Overlay` | `<div>` | Dialog | Backdrop. Click-outside does NOT close (forced by modal=true + alertdialog). |
| `Title` | `<div>` | Dialog | Dialog heading. Linked via `aria-labelledby` . |
| `Description` | `<div>` | Dialog | Supporting text. Linked via `aria-describedby` . |
| `Action` | `<button>` | AlertDialog | Primary / destructive confirmation. Closes the dialog on click. |
| `Cancel` | `<button>` | AlertDialog | Safe escape button. Closes the dialog on click. |
| Part | Element | Origin | Description |
| ------------- | ---------- | ----------- | - --------------------------------------------------------------------------- |
| `Provider` | none | AlertDialog | Root context. Forces `variant='alertdialog'` + `modal=true` on Dialog. |
| `Trigger` | `<button>` | Dialog | Opens the dialog on click. |
| `Content` | `<div>` | Dialog | Dialog container. Focus trap, scroll lock, dismissal layers integrated. |
| `Overlay` | `<div>` | Dialog | Backdrop. Click-outside does NOT close (forced by modal=true + alertdialog). |
| `Title` | `<div>` | Dialog | Dialog heading. Linked via `aria-labelledby` . |
| `Description` | `<div>` | Dialog | Supporting text. Linked via `aria-describedby` . |
| `Action` | `<button>` | AlertDialog | Primary / destructive confirmation. Closes the dialog on click. |
| `Cancel` | `<button>` | AlertDialog | Safe escape button. Closes the dialog on click. |
## Props
### `Provider`
| Prop | Type | Default | Description |
| ---------------------- | ----------------------------- | ------- | ----------- ----------------------------------------------- |
| `id` | `string` | auto | DOM id for the root. |
| `open` | `boolean` | `false` | Bindable open state. |
| `onOpenChange` | `(v: boolean) => void` | — | Called when open state changes. |
| `onOpenChangeComplete` | `(v: boolean) => void` | — | Called after the open/close animation settles. |
| `disabled` | `boolean` | `false` | Disables the trigger (the dialog stays closed). |
| Prop | Type | Default | Description |
| ---------------------- | ---------------------- | ------- | ----------------------------------------------- |
| `id` | `string` | auto | DOM id for the root. |
| `open` | `boolean` | `false` | Bindable open state. |
| `onOpenChange` | `(v: boolean) => void` | — | Called when open state changes. |
| `onOpenChangeComplete` | `(v: boolean) => void` | — | Called after the open/close animation settles. |
| `disabled` | `boolean` | `false` | Disables the trigger (the dialog stays closed). |
### `Action` / `Cancel`
Both accept every `<button>` attribute plus:
| Prop | Type | Default | Description |
| ------------ | ---------- | ----------------------- | - -------------------------------------------------- |
| `id` | `string` | auto | DOM id. |
| `aria-label` | `string` | `"Confirm"` / `"Cancel"` | Accessible name. Overrides the translated default. |
| Prop | Type | Default | Description |
| ------------ | -------- | ------------------------ | -------------------------------------------------- |
| `id` | `string` | auto | DOM id. |
| `aria-label` | `string` | `"Confirm"` / `"Cancel"` | Accessible name. Overrides the translated default. |
`Trigger` , `Content` , `Overlay` , `Title` , `Description` are re-exported from `Dialog` — see [Dialog's props ](../dialog/README.md ) for the complete list. Of particular interest:
@ -71,49 +69,49 @@ Both accept every `<button>` attribute plus:
## ARIA
| Part | Attribute | Value |
| ------- | ------------------- | - ------------------------------------------ |
| Trigger | `aria-haspopup` | `dialog` |
| Trigger | `aria-expanded` | `true` \| `false` |
| Trigger | `aria-controls` | ID of Content |
| Content | `role` | `alertdialog` |
| Content | `aria-modal` | `true` |
| Content | `aria-labelledby` | ID of Title |
| Content | `aria-describedby` | ID of Description |
| Overlay | `aria-hidden` | `true` |
| Title | `role` | `heading` |
| Title | `aria-level` | Number (default `2` ) |
| Action | `aria-label` | Translated `'Confirm'` (override via prop) |
| Cancel | `aria-label` | Translated `'Cancel'` (override via prop) |
| Part | Attribute | Value |
| ------- | ------------------ | ------------------------------------------ |
| Trigger | `aria-haspopup` | `dialog` |
| Trigger | `aria-expanded` | `true` \| `false` |
| Trigger | `aria-controls` | ID of Content |
| Content | `role` | `alertdialog` |
| Content | `aria-modal` | `true` |
| Content | `aria-labelledby` | ID of Title |
| Content | `aria-describedby` | ID of Description |
| Overlay | `aria-hidden` | `true` |
| Title | `role` | `heading` |
| Title | `aria-level` | Number (default `2` ) |
| Action | `aria-label` | Translated `'Confirm'` (override via prop) |
| Cancel | `aria-label` | Translated `'Cancel'` (override via prop) |
## Data Attributes
| Part | Attribute | Values |
| -------- | -- -------------------------- | --------- --------------------------------------------- |
| Trigger | `data-dialog-trigger` | Always present |
| Trigger | `data-state` | `open` \| `closed` |
| Content | `data-dialog-content` | Always present |
| Content | `data-state` | `open` \| `closed` |
| Content | `data-nested` | Present when nested |
| Content | `data-nested-open` | Present when a child dialog is open |
| Content | `data-starting-style` | Present during the open animation (one frame) |
| Content | `data-ending-style` | Present during the close animation |
| Overlay | `data-dialog-overlay` | Always present |
| Overlay | `data-state` | `open` \| `closed` |
| Action | `data-alert-dialog-action` | Always present |
| Cancel | `data-alert-dialog-cancel` | Always present |
| Part | Attribute | Values |
| ------- | -------------------------- | --------------------------------------------- |
| Trigger | `data-dialog-trigger` | Always present |
| Trigger | `data-state` | `open` \| `closed` |
| Content | `data-dialog-content` | Always present |
| Content | `data-state` | `open` \| `closed` |
| Content | `data-nested` | Present when nested |
| Content | `data-nested-open` | Present when a child dialog is open |
| Content | `data-starting-style` | Present during the open animation (one frame) |
| Content | `data-ending-style` | Present during the close animation |
| Overlay | `data-dialog-overlay` | Always present |
| Overlay | `data-state` | `open` \| `closed` |
| Action | `data-alert-dialog-action` | Always present |
| Cancel | `data-alert-dialog-cancel` | Always present |
Because `AlertDialog` composes `Dialog` , the Trigger / Content / Overlay / Title / Description emit the `data-dialog-*` attributes inherited from that component. The two AlertDialog-specific parts (`Action`, `Cancel` ) emit their own `data-alert-dialog-*` attributes so themes can style the primary vs secondary buttons independently.
## Keyboard
| Key | Action |
| ----------- | -------------------------------------------------------------------------------------- |
| Key | Action |
| ----------- | ------------------------------------------------------------------------------------------------------------------ |
| `Escape` | Closes (acts as Cancel). Default `escapeKeydownBehavior='close'` . Pass `'ignore'` to force explicit Action/Cancel. |
| `Tab` | Cycle focus within the dialog (trapped). |
| `Shift+Tab` | Cycle focus backwards. |
| `Enter` | Fires `onclick` on the focused button. |
| `Space` | Fires `onclick` on the focused button. |
| `Tab` | Cycle focus within the dialog (trapped). |
| `Shift+Tab` | Cycle focus backwards. |
| `Enter` | Fires `onclick` on the focused button. |
| `Space` | Fires `onclick` on the focused button. |
Focus lands on the first focusable child when the dialog opens (`Cancel` by DOM order convention). Focus returns to the trigger on close.
@ -121,28 +119,28 @@ Focus lands on the first focusable child when the dialog opens (`Cancel` by DOM
Default button labels resolve through the UIX lang system. The component catalog is owned by `morfo.translations` under `components.alert-dialog` .
| Key | English | Spanish |
| -------- | --------- | ------------ |
| `action` | `Confirm` | `Confirmar` |
| `cancel` | `Cancel` | `Cancelar` |
| Key | English | Spanish |
| -------- | --------- | ----------- |
| `action` | `Confirm` | `Confirmar` |
| `cancel` | `Cancel` | `Cancelar` |
Consumers override per-instance via the `aria-label` prop on `Action` / `Cancel` , or the visible label via `children` .
## Comparison
| Feature | Soma | Radix | Ark UI | bits-ui |
| ---------------------------------------------- | :--: | :---: | :----: | :-----: |
| `role="alertdialog"` emitted | ✅ | ✅ | ✅ | ✅ |
| Always modal | ✅ | ✅ | ✅ | ✅ |
| `Action` + `Cancel` as first-class parts | ✅ | ✅ | ✅ | ✅ |
| Click-outside disabled | ✅ | ✅ | ✅ | ✅ |
| Escape closes by default | ✅ | ✅ | ✅ | ✅ |
| Translated default button labels | ✅ | ❌ | ⚠️² | ❌ |
| Nested alert dialogs | ✅ | ✅ | ✅ | ✅ |
| Portal-agnostic (consumer picks) | ✅ | ✅ | ✅ | ✅ |
| `data-*` attribute contract | ✅ | ✅ | ✅ | ✅ |
| Shared anatomy with plain Dialog | ✅ | ❌ | ✅ | ✅ |
| Built-in Form integration (confirm on submit) | ✅³ | ❌ | ❌ | ❌ |
| Feature | Soma | Radix | Ark UI | bits-ui |
| --------------------------------------------- | :--: | :---: | :----: | :-----: |
| `role="alertdialog"` emitted | ✅ | ✅ | ✅ | ✅ |
| Always modal | ✅ | ✅ | ✅ | ✅ |
| `Action` + `Cancel` as first-class parts | ✅ | ✅ | ✅ | ✅ |
| Click-outside disabled | ✅ | ✅ | ✅ | ✅ |
| Escape closes by default | ✅ | ✅ | ✅ | ✅ |
| Translated default button labels | ✅ | ❌ | ⚠️² | ❌ |
| Nested alert dialogs | ✅ | ✅ | ✅ | ✅ |
| Portal-agnostic (consumer picks) | ✅ | ✅ | ✅ | ✅ |
| `data-*` attribute contract | ✅ | ✅ | ✅ | ✅ |
| Shared anatomy with plain Dialog | ✅ | ❌ | ✅ | ✅ |
| Built-in Form integration (confirm on submit) | ✅³ | ❌ | ❌ | ❌ |
² Ark has a machine-level translator seam but no built-in English/Spanish strings.
³ Demonstrated on the test page: `onValidSubmit` returns a Promise gated on a confirmation dialog.
@ -167,9 +165,7 @@ Consumers override per-instance via the `aria-label` prop on `Action` / `Cancel`
< AlertDialog.Overlay / >
< AlertDialog.Content >
< AlertDialog.Title > Delete account?< / AlertDialog.Title >
< AlertDialog.Description >
All your data will be permanently removed.
< / AlertDialog.Description >
< AlertDialog.Description > All your data will be permanently removed.< / AlertDialog.Description >
< AlertDialog.Cancel > Keep it< / AlertDialog.Cancel >
< AlertDialog.Action onclick = {deleteAccount} > Delete< / AlertDialog.Action >
< / AlertDialog.Content >
@ -179,9 +175,7 @@ Consumers override per-instance via the `aria-label` prop on `Action` / `Cancel`
### Force explicit Action/Cancel (Escape ignored)
```svelte
< AlertDialog.Content escapeKeydownBehavior = "ignore" >
…
< / AlertDialog.Content >
< AlertDialog.Content escapeKeydownBehavior = "ignore" > …< / AlertDialog.Content >
```
### Gating a form submission
@ -189,7 +183,7 @@ Consumers override per-instance via the `aria-label` prop on `Action` / `Cancel`
```svelte
< script lang = "ts" >
import { AlertDialog, Form, Field } from '$soma/components';
import { createForm } from '$soma/components/form ';
import { createForm } from '$libs/forms ';
let confirmOpen = $state(false);
let resolve: ((ok: boolean) => void) | null = null;
@ -208,14 +202,17 @@ Consumers override per-instance via the `aria-label` prop on `Action` / `Cancel`
< Form.Provider { form } > …< Form.Submit > Delete< / Form.Submit > < / Form.Provider >
< AlertDialog.Provider bind:open = {confirmOpen}
onOpenChange={(v) => !v & & resolve?.(false)}
>
< AlertDialog.Provider bind:open = {confirmOpen} onOpenChange = {(v) = > !v & & resolve?.(false)}>
< AlertDialog.Overlay / >
< AlertDialog.Content >
< AlertDialog.Title > Really?< / AlertDialog.Title >
< AlertDialog.Cancel > Cancel< / AlertDialog.Cancel >
< AlertDialog.Action onclick = {() = > { resolve?.(true); confirmOpen = false; }}>
< AlertDialog.Action
onclick={() => {
resolve?.(true);
confirmOpen = false;
}}
>
Confirm
< / AlertDialog.Action >
< / AlertDialog.Content >