Alert Dialog
stimeo--alert-dialog
An interrupting confirmation: no backdrop close, and confirm and cancel each report back.
Implements the WAI-ARIA Alert Dialog pattern. Keeping focus inside, stopping background scrolling, holding the background inert and restoring focus all work as they do in the plain dialog. Two things differ, because this one interrupts. Clicking the backdrop never closes it. Confirming and cancelling each report back separately, and a cancel tells you whether it came from Esc or from the control. Opening puts focus on the least destructive choice. All you write is the message and what happens on confirm.
Delete this item?
This action cannot be undone.
Keyboard
| Key | Action |
|---|---|
| Esc | Cancel the dialog and return focus to the opener. |
| Tab / Shift+Tab | Cycle focus within the dialog (focus trap). |
<%# Markup for the alert-dialog demo.
role="alertdialog" conveys urgency and interrupts until an explicit confirm/cancel
response is given. Unlike a plain dialog it does not close on a backdrop click.
The library handles focus move, focus trap, Esc (cancel), background scroll lock,
background inert, returning focus to the trigger, and firing confirm / cancel
events. Styling lives in demo.css. %>
<div class="alert-dialog" data-controller="stimeo--alert-dialog">
<button
type="button"
class="demo-trigger"
data-stimeo--alert-dialog-target="trigger"
data-action="click->stimeo--alert-dialog#open">
<%= t("components.alert_dialog.demo.open_button") %>
</button>
<%# A live region that announces the latest response; demo.js reflects confirm / cancel.
For bilingual support the copy is resolved with I18n server-side and passed via
data-* attributes, which demo.js only reads (never hardcoded in JS). %>
<p
class="alert-dialog__status"
role="status"
data-alert-dialog-status
data-confirm-message="<%= t("components.alert_dialog.demo.confirmed_message") %>"
data-cancel-message-user="<%= t(
"components.alert_dialog.demo.cancelled_message",
reason: t("components.alert_dialog.demo.cancel_reason_user")
) %>"
data-cancel-message-escape="<%= t(
"components.alert_dialog.demo.cancelled_message",
reason: t("components.alert_dialog.demo.cancel_reason_escape")
) %>"
hidden></p>
<%# The role="alertdialog" + aria-modal region. Starts hidden (closed).
aria-labelledby points to the title, aria-describedby to the body message. %>
<div
class="alert-dialog__backdrop"
role="alertdialog"
aria-modal="true"
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-desc"
data-stimeo--alert-dialog-target="dialog"
hidden>
<div class="alert-dialog__panel">
<h3 id="alert-dialog-title" class="alert-dialog__title">
<%= t("components.alert_dialog.demo.confirm_title") %>
</h3>
<p id="alert-dialog-desc"><%= t("components.alert_dialog.demo.confirm_desc") %></p>
<div class="alert-dialog__actions">
<%# Default focus goes to the least-destructive action: Cancel. %>
<button
type="button"
class="demo-trigger"
data-stimeo--alert-dialog-target="initialFocus"
data-action="click->stimeo--alert-dialog#cancel">
<%= t("components.alert_dialog.demo.cancel") %>
</button>
<button
type="button"
class="demo-trigger alert-dialog__danger"
data-action="click->stimeo--alert-dialog#confirm">
<%= t("components.alert_dialog.demo.confirm") %>
</button>
</div>
</div>
</div>
</div>
/* The confirm button is a destructive action, so show it in a warning color (visual only). */
.alert-dialog__danger {
border-color: var(--danger-700);
background: var(--danger-700);
color: var(--white);
}
.alert-dialog__status {
margin: 0.75rem 0 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.alert-dialog__status[hidden] {
display: none;
}
/* Use the role="alertdialog" region as a centered backdrop. */
.alert-dialog__backdrop {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgb(0 0 0 / 0.5);
z-index: 50;
}
/* Setting display explicitly overrides the hidden attribute's default display:none, so
re-declare display:none while closed to honor the library's hidden control. */
.alert-dialog__backdrop[hidden] {
display: none;
}
.alert-dialog__panel {
width: min(28rem, 100%);
padding: 1.5rem;
background: var(--surface-card);
border-radius: 0.5rem;
box-shadow: 0 20px 50px rgb(15 23 42 / 0.25);
}
.alert-dialog__title {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
.alert-dialog__actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1.5rem;
}
// Consumer-side JS for the alert-dialog demo (optional).
// The library fires stimeo--alert-dialog:confirm on confirm and
// stimeo--alert-dialog:cancel (with detail { reason: "user" | "escape" }) on
// cancel. The consumer subscribes to decide "what to do on confirm". Here we only
// reflect the latest response in the status live region (the real work is the
// subscriber's responsibility — this is just an example).
// For bilingual support the copy is resolved server-side via I18n and read from the
// status element's data-* attributes (never hardcoded in JS).
document.addEventListener("stimeo--alert-dialog:confirm", () => {
const status = document.querySelector("[data-alert-dialog-status]");
if (status) showStatus(status.dataset.confirmMessage);
});
document.addEventListener("stimeo--alert-dialog:cancel", (event) => {
const status = document.querySelector("[data-alert-dialog-status]");
if (!status) return;
// reason is "escape" (cancelled with the Esc key) or "user" (the Cancel button).
const message =
event.detail.reason === "escape"
? status.dataset.cancelMessageEscape
: status.dataset.cancelMessageUser;
showStatus(message);
});
// Show the latest response message in the status element.
function showStatus(message) {
const status = document.querySelector("[data-alert-dialog-status]");
if (!status) return;
status.textContent = message;
status.hidden = false;
}
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--alert-dialog"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
The button that opens the alert dialog; focus returns here on close. | data-stimeo--alert-dialog-target="trigger" |
dialog
required
|
The role="alertdialog" modal element shown/hidden via hidden; holds the message and confirm/cancel buttons. |
data-stimeo--alert-dialog-target="dialog" |
initialFocus
|
The element focused on open (by convention the least destructive action, e.g. Cancel). | data-stimeo--alert-dialog-target="initialFocus" |
Actions
| Name | Description | Action |
|---|---|---|
cancel
|
Closes the dialog and dispatches cancel with reason user. |
stimeo--alert-dialog#cancel |
confirm
|
Closes the dialog and dispatches confirm. |
stimeo--alert-dialog#confirm |
open
|
Opens the dialog, moves focus to initialFocus, traps Tab, and locks background scroll; never closes on backdrop. | stimeo--alert-dialog#open |
Events
| Name | Description | Event |
|---|---|---|
cancel
|
Fires when cancelled; detail.reason is user (cancel action) or escape (Escape key). |
stimeo--alert-dialog:cancel |
confirm
|
Fires when the user confirms the action. | stimeo--alert-dialog:confirm |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
hidden |
Dialog | Present when closed; removed when open. |
inert |
Background siblings | Added to elements outside the dialog while open. |
overflow |
document.body | Set to hidden while open (scroll lock). |