Alert Dialog
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>.
Basic
Confirm/cancel pattern with title and description.
<button class="btn" data-variant="outline"
data-alert-dialog-trigger="my-alert">
Delete Account
</button>
<dialog id="my-alert" class="alert-dialog"
role="alertdialog" aria-modal="true"
aria-labelledby="ad-title" aria-describedby="ad-desc">
<div class="alert-dialog-content">
<div class="alert-dialog-header">
<h2 class="alert-dialog-title" id="ad-title">
Are you absolutely sure?
</h2>
<p class="alert-dialog-description" id="ad-desc">
This action cannot be undone.
</p>
</div>
<div class="alert-dialog-footer">
<button class="btn" data-variant="outline"
data-alert-dialog-close>Cancel</button>
<button class="btn" data-variant="destructive"
data-alert-dialog-close>Delete</button>
</div>
</div>
</dialog>
Confirmation
Non-destructive confirmation with standard action button.
<button class="btn" data-alert-dialog-trigger="publish-dialog">
Publish Post
</button>
<dialog id="publish-dialog" class="alert-dialog" role="alertdialog"
aria-modal="true" aria-labelledby="..." aria-describedby="...">
...
</dialog>
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);
}
}
.alert-dialog-content {
padding: 1.5rem;
}
.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;
}
}
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.
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 = '';
/* 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', () => {
if (dialog._trigger) dialog._trigger.focus();
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });