Toast
stimeo--toast
An accessible notification queue. Implements WAI-ARIA live regions, maximum limits, and pausing on hover/focus.
The stimeo--toast controller implements highly accessible notification queues (WAI-ARIA live regions). When shown, a notification is appended to a list target. Auto-dismiss timers pause on hover (mouseenter) or keyboard focus (focusin) to comply with WCAG 2.2.1 (Timing Adjustable), and resume afterwards. Dynamic updates (e.g. Turbo Streams) are naturally handled via target lifecycle connections.
Keyboard
| Key | Action |
|---|---|
| Esc | Immediately dismiss the focused toast item. |
| Tab / Shift+Tab | Move focus into the toast item to pause its timer. |
<%# Markup for the toast demo.
The single source used for both the live render and the copy-paste code.
stimeo--toast provides max-count management, WAI-ARIA live region support, and
pausing the auto-dismiss timer on hover or keyboard focus.
Showing a toast is done with attributes alone (no hand-written JS):
put data-action="click->stimeo--toast#show" plus
data-stimeo--toast-body-param / data-stimeo--toast-type-param on the trigger button.
For advanced cases where you want to trigger it remotely from JS, the controller
element keeps show->stimeo--toast#show, so CustomEvent('show', { detail }) works too. %>
<div
class="toast-demo"
data-controller="stimeo--toast"
data-stimeo--toast-duration-value="5000"
data-stimeo--toast-max-value="3"
data-action="show->stimeo--toast#show">
<div class="toast-demo__triggers">
<button
type="button"
class="toast-demo__btn toast-demo__btn--status"
data-action="click->stimeo--toast#show"
data-stimeo--toast-body-param="<%= t("components.toast.demo.status_body") %>"
data-stimeo--toast-type-param="status">
<%= t("components.toast.demo.show_status") %>
</button>
<button
type="button"
class="toast-demo__btn toast-demo__btn--alert"
data-action="click->stimeo--toast#show"
data-stimeo--toast-body-param="<%= t("components.toast.demo.alert_body") %>"
data-stimeo--toast-type-param="alert">
<%= t("components.toast.demo.show_alert") %>
</button>
</div>
<%# The live region is a descendant of the controller. The controller element only
needs to contain the trigger and the list/template; role="region" goes on this
inner container. %>
<div
id="playground-toast"
role="region"
aria-label="<%= t("components.toast.demo.dismiss_label") %>"
class="toast-container">
<%# The element where notifications are placed. %>
<ol class="toast-list" data-stimeo--toast-target="list"></ol>
<%# Template used to create new toasts. %>
<template data-stimeo--toast-target="template">
<li
role="status"
class="toast-item"
data-stimeo--toast-target="item"
data-action="mouseenter->stimeo--toast#pause
mouseleave->stimeo--toast#resume
focusin->stimeo--toast#pause
focusout->stimeo--toast#resume
keydown->stimeo--toast#onKeydown"
tabindex="0">
<div class="toast-item__content">
<span data-toast-slot="body" class="toast-item__body"></span>
<button
type="button"
class="toast-item__dismiss"
data-action="click->stimeo--toast#dismiss"
aria-label="<%= t("components.toast.demo.dismiss_label") %>">
<svg
class="toast-item__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
</li>
</template>
</div>
</div>
/*
* Presentation-only styles for the toast demo.
* Toned-down, semi-flat styling (no glassmorphism) to blend with the wireframe look
* (plain white background) of the other components.
*/
.toast-demo {
margin: 1rem 0;
}
.toast-demo__triggers {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.toast-demo__btn {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid var(--border-interactive);
border-radius: 0.375rem;
background: var(--surface-card);
transition: all 0.15s ease;
}
.toast-demo__btn:hover {
border-color: var(--accent);
}
.toast-demo__btn--status:hover {
border-color: var(--leaf-500);
color: var(--leaf-500);
}
.toast-demo__btn--alert:hover {
border-color: var(--danger-500);
color: var(--danger-500);
}
/* Container that positions the whole toast stack. */
.toast-container {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
pointer-events: none; /* The container as a whole is click-through. */
}
.toast-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
width: min(22rem, 90vw);
}
/* Each toast item - plain white background to blend with other popups and dropdown lists. */
.toast-item {
pointer-events: auto; /* The toast item itself is clickable. */
position: relative;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
outline: none;
}
.toast-item[role="alert"] {
border-left: 4px solid var(--danger-500);
}
.toast-item[role="status"] {
border-left: 4px solid var(--leaf-500);
}
.toast-item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* Paused state when data-paused="true" (slightly gray). */
.toast-item[data-paused="true"] {
background: var(--surface-subtle);
}
/* Lifecycle states used for transitions. */
.toast-item[data-state="entering"] {
opacity: 0;
transform: translateY(0.5rem);
}
.toast-item[data-state="visible"] {
opacity: 1;
transform: translateY(0);
}
.toast-item[data-state="leaving"] {
opacity: 0;
transform: translateY(-0.25rem);
}
.toast-item__content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
gap: 1rem;
}
.toast-item__body {
font-size: 0.9rem;
color: var(--fg);
line-height: 1.4;
flex-grow: 1;
}
.toast-item__dismiss {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
background: transparent;
border: none;
color: var(--color-text-subtle);
border-radius: 9999px;
cursor: pointer;
transition: all 0.15s ease;
}
.toast-item__dismiss:hover {
background: var(--surface-subtle);
color: var(--color-text-muted);
}
.toast-item__icon {
width: 0.85rem;
height: 0.85rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
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--toast"
Targets
| Name | Description | Attribute |
|---|---|---|
list
required
|
The live-region list (e.g. <ol>) that new toast items are appended to. |
data-stimeo--toast-target="list" |
template
|
The <template> cloned to create each toast, with a body slot to interpolate the message. |
data-stimeo--toast-target="template" |
item
|
A single toast (role="status"/alert); pauses on hover/focus, dismissable, and tracks data-state for transitions. |
data-stimeo--toast-target="item" |
Values
| Name | Description | Attribute |
|---|---|---|
duration
|
Auto-dismiss timeout in milliseconds; 0 disables auto-dismiss (default 0). | data-stimeo--toast-duration-value |
max
|
Maximum simultaneous toasts; the oldest are removed past this limit (default 3). | data-stimeo--toast-max-value |
Actions
| Name | Description | Action |
|---|---|---|
dismiss
|
Dismisses the toast containing the trigger (with reason user). |
stimeo--toast#dismiss |
onKeydown
|
Dismisses the focused toast when Escape is pressed. | stimeo--toast#onKeydown |
pause
|
Pauses the auto-dismiss timer on hover or focus, snapshotting the remaining time (WCAG 2.2.1). | stimeo--toast#pause |
resume
|
Resumes the auto-dismiss timer once both hover and focus have been released. | stimeo--toast#resume |
show
|
Clones the template, interpolates body text from a param or event detail, sets the live-region role, and appends a new toast. | stimeo--toast#show |
Events
| Name | Description | Event |
|---|---|---|
dismiss
|
Fires when a toast is removed; detail carries the item element and reason (user or timeout). |
stimeo--toast:dismiss |
show
|
Fires when a new toast is shown; detail carries the created item element. | stimeo--toast:show |
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 |
|---|---|---|
role="status" |
Toast item | Announced by screen readers as a polite status message (default). |
role="alert" |
Toast item | Announced by screen readers as an assertive, urgent message. |
data-paused="true" |
Toast item | Indicates the auto-dismiss timer is paused due to hover or focus. |
data-state="entering | visible | leaving" |
Toast item | Reflects the current lifecycle state, allowing CSS transition integration. |