Confirm Bridge
stimeo--confirm
Replaces Turbo's native confirm() with an accessible alert dialog.
The stimeo--confirm controller replaces the native window.confirm() that Turbo uses for data-turbo-confirm with an accessible alert dialog (WAI-ARIA APG Alert Dialog pattern), reusing the shared FocusTrap so the trap, focus restore, and Escape-to-cancel are not reimplemented. On connect it swaps Turbo.config.forms.confirm for a Promise-returning method and restores the original on disconnect (Turbo navigation included), so registration never leaks or stacks. It can also intercept any link/button via the request action and continue the original submit/navigation only when confirmed. The least destructive button (cancel) takes initial focus; Escape cancels. When no dialog is present it degrades to native confirm. It dispatches stimeo--confirm:open and :resolve. Behavior only — the dialog markup and styling belong to this Playground.
Delete account?
Keyboard
| Key | Action |
|---|---|
| Esc | Cancels (resolves false) and closes the dialog. |
| Tab / Shift+Tab | Cycles focus within the dialog (focus trap). |
| Enter | Activates the focused confirm or cancel button. |
<%# Markup for the confirm-bridge demo.
The controller intercepts the danger button (request action), shows the accessible
alert dialog with the message, and only continues the form submit when confirmed.
The dialog reuses the shared FocusTrap (trap, restore, Escape=cancel). This
Playground has no backend, so demo.js cancels the real submit and reports the
outcome from the resolve event. %>
<div class="confirm-demo" data-controller="stimeo--confirm"
data-stimeo--confirm-confirm-label-value="<%= t("components.confirm.demo.confirm_label") %>"
data-stimeo--confirm-cancel-label-value="<%= t("components.confirm.demo.cancel_label") %>">
<form class="confirm-demo__form" action="#" method="post" data-confirm-demo="form">
<button type="submit" class="demo-trigger demo-trigger--danger"
data-action="click->stimeo--confirm#request"
data-stimeo--confirm-message-param="<%= t("components.confirm.demo.message") %>">
<%= t("components.confirm.demo.trigger") %>
</button>
</form>
<p class="confirm-demo__result" data-confirm-demo="result" role="status" aria-live="polite"
data-confirmed="<%= t("components.confirm.demo.confirmed") %>"
data-cancelled="<%= t("components.confirm.demo.cancelled") %>"></p>
<div class="confirm-demo__backdrop" data-stimeo--confirm-target="dialog"
role="alertdialog" aria-modal="true" aria-labelledby="confirm-title"
aria-describedby="confirm-message" hidden>
<div class="confirm-demo__dialog">
<h2 id="confirm-title" data-stimeo--confirm-target="title">
<%= t("components.confirm.demo.title") %>
</h2>
<p id="confirm-message" data-stimeo--confirm-target="message"></p>
<div class="confirm-demo__actions">
<button type="button" class="demo-trigger" data-stimeo--confirm-target="cancel"
data-action="click->stimeo--confirm#cancel"></button>
<button type="button" class="demo-trigger demo-trigger--danger"
data-stimeo--confirm-target="confirm"
data-action="click->stimeo--confirm#confirm"></button>
</div>
</div>
</div>
</div>
/*
* Presentation-only styles for the confirm-bridge demo.
* The library only toggles the dialog's hidden attribute and manages focus; this
* CSS owns the modal overlay, the dialog box, and the danger-button affordance.
*/
.confirm-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 32rem;
}
.demo-trigger--danger {
border-color: var(--danger-500);
color: var(--color-accent);
}
.confirm-demo__result {
margin: 0;
min-height: 1.25rem;
font-size: 0.95rem;
color: var(--color-text-muted);
}
.confirm-demo__backdrop {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgb(15 23 42 / 0.5);
z-index: 50;
}
.confirm-demo__backdrop[hidden] {
display: none;
}
.confirm-demo__dialog {
width: min(28rem, calc(100vw - 2rem));
padding: 1.25rem;
border-radius: 0.75rem;
background: var(--surface, var(--surface-card));
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.2);
}
.confirm-demo__dialog h2 {
margin: 0 0 0.5rem;
font-size: 1.1rem;
}
.confirm-demo__actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
// Consumer-side JS for the confirm-bridge demo (demo-only).
// On confirm, the controller continues the original action by submitting the form.
// This Playground has no backend, so we cancel the real submit and instead report
// the outcome (confirmed / cancelled) from the controller's resolve event.
const form = document.querySelector("[data-confirm-demo='form']");
const result = document.querySelector("[data-confirm-demo='result']");
if (form) {
form.addEventListener("submit", (event) => event.preventDefault());
}
document.addEventListener("stimeo--confirm:resolve", (event) => {
if (!result) return;
result.textContent = event.detail.confirmed
? result.dataset.confirmed
: result.dataset.cancelled;
});
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--confirm"
Targets
| Name | Description | Attribute |
|---|---|---|
dialog
required
|
The alert dialog element (role="alertdialog") that is trapped and shown. |
data-stimeo--confirm-target="dialog" |
title
|
Optional heading element naming the dialog. | data-stimeo--confirm-target="title" |
message
|
The element the confirmation message is written into. | data-stimeo--confirm-target="message" |
confirm
|
The confirm button; its label is set from confirmLabel. |
data-stimeo--confirm-target="confirm" |
cancel
|
The cancel button; its label is set from cancelLabel. |
data-stimeo--confirm-target="cancel" |
Values
| Name | Description | Attribute |
|---|---|---|
confirmLabel
|
Label for the confirm button (default OK). |
data-stimeo--confirm-confirm-label-value |
cancelLabel
|
Label for the cancel button (default Cancel). |
data-stimeo--confirm-cancel-label-value |
initialFocus
|
Which button gets initial focus: cancel or confirm (default cancel). |
data-stimeo--confirm-initial-focus-value |
Actions
| Name | Description | Action |
|---|---|---|
confirm
|
Resolves the pending confirmation as true and closes. |
stimeo--confirm#confirm |
cancel
|
Resolves the pending confirmation as false and closes. |
stimeo--confirm#cancel |
request
|
Intercepts a click, confirms, then continues the original action. | stimeo--confirm#request |
Events
| Name | Description | Event |
|---|---|---|
open
|
Fires when the dialog opens; detail carries the message. |
stimeo--confirm:open |
resolve
|
Fires when settled; detail carries confirmed (boolean). |
stimeo--confirm:resolve |
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 | Toggled to show/hide the confirm dialog. |