You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/alert-dialog
dev fe61f5be9b
Add alert dialog provider coverage
5 months ago
..
components sema/morfo/eidos: typed selector builder + dialog wrapper + emerge color 5 months ago
README.md Move simple utility translations to morfo 5 months ago
alert-dialog-provider.svelte.test.ts Add alert dialog provider coverage 5 months ago
alert-dialog-provider.svelte.ts Normalize Soma root service references 5 months ago
exports.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
index.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
langs.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
types.ts sema/morfo/eidos: typed selector builder + dialog wrapper + emerge color 5 months ago

README.md

AlertDialog

A modal dialog with role="alertdialog" used for destructive or consequential confirmations (delete, overwrite, discard). It is a thin specialisation of Dialog — the state machinery, focus trap, scroll lock and portal layers are all inherited unchanged.

Three things make an AlertDialog different from a plain Dialog:

  1. Semantics. Content emits role="alertdialog" so assistive technology announces it with alert semantics.
  2. Modality is fixed. modal=true and variant='alertdialog' are forced by AlertDialog.Provider. Non-modal alert dialogs are not a concept.
  3. No outside dismissal. Clicking the overlay does not close the dialog — the user must pick Action or Cancel explicitly. This matches the WAI-ARIA Authoring Practices for alert dialogs.

Anatomy

<AlertDialog.Provider bind:open>
	<AlertDialog.Trigger>Delete account</AlertDialog.Trigger>

	<AlertDialog.Overlay />
	<AlertDialog.Content>
		<AlertDialog.Title>Delete account?</AlertDialog.Title>
		<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>
</AlertDialog.Provider>

Wrap Overlay + Content in a Portal for body-level rendering when z-index stacking demands it.

By convention, place Cancel before Action in DOM order: the focus scope lands on the first focusable element when the dialog opens, so Cancel (the safe choice) receives initial focus — the WAI-ARIA recommendation for destructive confirmations.

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.

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).

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.

Trigger, Content, Overlay, Title, Description are re-exported from Dialog — see Dialog's props for the complete list. Of particular interest:

  • AlertDialog.Content defaults escapeKeydownBehavior to 'close' (Escape acts as Cancel — matches WAI-ARIA and Radix / Ark / bits). Pass escapeKeydownBehavior='ignore' for the "must click a button" variant.
  • AlertDialog.Content also accepts interactOutsideBehavior, but it is always resolved to ignore because modal=true and variant='alertdialog' both force it.

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)

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

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
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.

Focus lands on the first focusable child when the dialog opens (Cancel by DOM order convention). Focus returns to the trigger on close.

i18n

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

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) ✅³ ❌ ❌ ❌

² 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.

Usage

Basic destructive confirmation

<script lang="ts">
	import { AlertDialog } from '$soma/components';

	let open = $state(false);
	function deleteAccount() {
		/* … */
	}
</script>

<AlertDialog.Provider bind:open>
	<AlertDialog.Trigger>Delete account</AlertDialog.Trigger>

	<AlertDialog.Overlay />
	<AlertDialog.Content>
		<AlertDialog.Title>Delete account?</AlertDialog.Title>
		<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>
</AlertDialog.Provider>

Force explicit Action/Cancel (Escape ignored)

<AlertDialog.Content escapeKeydownBehavior="ignore">
	…
</AlertDialog.Content>

Gating a form submission

<script lang="ts">
	import { AlertDialog, Form, Field } from '$soma/components';
	import { createForm } from '$soma/components/form';

	let confirmOpen = $state(false);
	let resolve: ((ok: boolean) => void) | null = null;

	const form = createForm({
		defaults: { email: '' },
		async onValidSubmit(values) {
			const ok = await new Promise<boolean>((r) => {
				resolve = r;
				confirmOpen = true;
			});
			if (ok) await api.delete(values.email);
		}
	});
</script>

<Form.Provider {form}>…<Form.Submit>Delete</Form.Submit></Form.Provider>

<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; }}>
			Confirm
		</AlertDialog.Action>
	</AlertDialog.Content>
</AlertDialog.Provider>

Implementation notes

  • AlertDialog.Provider is a ~30-line Svelte file that creates a DialogProvider with variant='alertdialog' and modal=true. No separate state class — the Dialog state machine is authoritative.
  • Action and Cancel call DialogProvider.handleClose() on click. Consumer onclick handlers merged via mergeProps run first; the close fires last.
  • Because AlertDialog piggybacks on Dialog's context, Dialog.* parts still work inside an <AlertDialog.Provider> — but consumer code should stick to the AlertDialog.* alias for clarity.

Powered by TurnKey Linux.