Alert DialogMOL
A modal dialog that interrupts the user and requires a response. Unlike a standard dialog, users cannot dismiss it by clicking the backdrop or pressing Escape - they must choose an action. Built on native <dialog>.
On this page (6)
§Basic
Confirm/cancel pattern with title and description.
§Confirmation
Non-destructive confirmation with standard action button.
§Density
Set data-density on the {''} root to scale the content padding. A whitespace policy, not a zoom: only padding scales (ratio 0.75 / 1 / 1.25), typography stays identical. comfortable matches the unsized default.
§States
Named states via the shared State API, driven per instance through the bound api:
default- closed (the authored state; only close buttons or the API close it - Escape is blocked)open- shown modally viashowModal()
The demo element carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/alert-dialog-{state}.png.
Machine contract - verified against alert-dialog.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
open | boolean | true, false | false | Shown via showModal(); hidden via close() (native open property). |
§CSS view file
Entry/exit animation
@layer components { dialog.alert-dialog { border: none; border-radius: var(--radius-xl); background: var(--background); color: var(--foreground); padding: 0; max-width: 28rem; width: calc(100% - 2rem); box-shadow: var(--shadow-lg); margin: auto; position: fixed; inset: 0; /* Entry/exit animation */ opacity: 0; transform: translateY(-0.5rem) scale(0.98); transition: opacity 200ms ease, transform 200ms ease, display 200ms allow-discrete; &[open] { opacity: 1; transform: translateY(0) scale(1); } &::backdrop { background: oklch(0 0 0 / 0); backdrop-filter: blur(0px); transition: all 200ms ease, display 200ms allow-discrete; } &[open]::backdrop { background: oklch(0 0 0 / 0.45); backdrop-filter: blur(3px); } /* Block Escape key - user must choose an action */ &::backdrop { pointer-events: auto; } } @starting-style { dialog.alert-dialog[open] { opacity: 0; transform: translateY(-0.5rem) scale(0.98); } dialog.alert-dialog[open]::backdrop { background: oklch(0 0 0 / 0); backdrop-filter: blur(0px); } } /* -- Scroll lock ------------------------------------------- */ /* Page behind stays put while the alert is modal (see dialog.css): `:modal` + overflow:hidden freezes the viewport at its current offset. */ html:has(dialog.alert-dialog:modal) { overflow: hidden; scrollbar-gutter: stable; } .alert-dialog-content { padding: 1.5rem; } /* -- Density ------------------------------------------------ data-density on the .alert-dialog root scales the content padding (0.75 / 1 / 1.25 of the 1.5rem default); comfortable matches the unsized default. */ .alert-dialog:where([data-density="compact"]) .alert-dialog-content { padding: 1rem; } .alert-dialog:where([data-density="comfortable"]) .alert-dialog-content { padding: 1.5rem; } .alert-dialog:where([data-density="spacious"]) .alert-dialog-content { padding: 2rem; } .alert-dialog-header { margin-bottom: 1.25rem; } .alert-dialog-title { margin: 0; font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; } .alert-dialog-description { margin: 0.5rem 0 0; font-size: 0.875rem; color: var(--muted-foreground); line-height: 1.5; } .alert-dialog-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }}/* Accessibility: suppress motion for users who request it (REQUIRED for all components - AGENTS.md "Accessibility CSS"). Near-zero duration instead of `none` keeps transitionend/animationend (and discrete display flips) firing so JS state machines that await them keep working. */@media (prefers-reduced-motion: reduce) { @layer components { .alert-dialog, .alert-dialog *, .alert-dialog::before, .alert-dialog::after, .alert-dialog *::before, .alert-dialog *::after, .alert-dialog::backdrop, .alert-dialog-content, .alert-dialog-content *, .alert-dialog-content::before, .alert-dialog-content::after, .alert-dialog-content *::before, .alert-dialog-content *::after, .alert-dialog-content::backdrop, .alert-dialog-header, .alert-dialog-header *, .alert-dialog-header::before, .alert-dialog-header::after, .alert-dialog-header *::before, .alert-dialog-header *::after, .alert-dialog-header::backdrop, .alert-dialog-title, .alert-dialog-title *, .alert-dialog-title::before, .alert-dialog-title::after, .alert-dialog-title *::before, .alert-dialog-title *::after, .alert-dialog-title::backdrop, .alert-dialog-description, .alert-dialog-description *, .alert-dialog-description::before, .alert-dialog-description::after, .alert-dialog-description *::before, .alert-dialog-description *::after, .alert-dialog-description::backdrop, .alert-dialog-footer, .alert-dialog-footer *, .alert-dialog-footer::before, .alert-dialog-footer::after, .alert-dialog-footer *::before, .alert-dialog-footer *::after, .alert-dialog-footer::backdrop { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; } }}§JavaScript view file
Alert Dialog
// -- Alert Dialog ----------------------------------------------// Wires [data-alert-dialog-trigger] buttons to <dialog class="alert-dialog">.// Unlike regular dialogs: no backdrop-close, Escape key blocked. Named-state// API per AGENTS.md "State API" (camelCase alert-dialog → alertDialogApi).// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals } from '../../shared/state-api.js';const df$ = defussGlobals();const alertDialogStates = ['default', 'open'];/** * UI side of setState: 'default' closes, 'open' opens modally. Escape and * backdrop dismissal stay blocked by the listeners below; closing is * programmatic only (close buttons / api). */function triggerStateChange(dialog, stateName, _config) { switch (stateName) { case 'default': if (dialog.open) dialog.close(); break; case 'open': if (!dialog.open) dialog.showModal(); break; }}/** Registry-level API; pass the dialog element explicitly. Unknown names throw. */export const alertDialogApi = { setState(dialog, stateName, config = {}) { if (!alertDialogStates.includes(stateName)) { throw new Error(`alert-dialog: unknown state "${stateName}" (supported: ${alertDialogStates.join(', ')})`); } triggerStateChange(dialog, stateName, config); // state lives on the ELEMENT, not the module (multiple dialogs per page) dialog.dataset.stateName = stateName; dialog._stateConfig = config; }, getState(dialog) { return { name: dialog.dataset.stateName || 'default', config: dialog._stateConfig ?? {} }; },};df$.alertDialogApi = alertDialogApi;df$.alertDialogStates = alertDialogStates;function init() {/* Wire triggers */document.querySelectorAll('[data-alert-dialog-trigger]:not([data-init])').forEach((trigger) => { trigger.dataset.init = ''; const dialog = document.getElementById(trigger.dataset.alertDialogTrigger); if (!dialog) return; trigger.addEventListener('click', () => { dialog._trigger = trigger; dialog.showModal(); });});/* Wire close buttons and block Escape */document.querySelectorAll('dialog.alert-dialog:not([data-init])').forEach((dialog) => { dialog.dataset.init = ''; // bind-scope the api per instance: `$('#confirm').api.setState('open')` dialog.api = { setState: (stateName, config) => alertDialogApi.setState(dialog, stateName, config), getState: () => alertDialogApi.getState(dialog), }; /* Block Escape key */ dialog.addEventListener('cancel', (e) => { e.preventDefault(); }); /* Wire close buttons */ dialog.querySelectorAll('[data-alert-dialog-close]').forEach((btn) => { btn.addEventListener('click', () => { dialog.close(); }); }); /* Return focus to trigger */ dialog.addEventListener('close', () => { // `close` fires AFTER the exit transition (display allow-discrete), so a // fast re-open can beat it - a stale event must not downgrade an open // dialog back to 'default' or yank focus out of it while it's showing. if (dialog.open) return; // reflect the actual UI state: close buttons (the only close path) or // setState('default') land the dialog back at 'default' dialog.dataset.stateName = 'default'; if (dialog._trigger) dialog._trigger.focus(); });});}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub