トースト
stimeo--toast
アクセシブルな通知キュー。WAI-ARIA ライブ領域、最大数制限、ホバー/フォーカスによるタイマー一時停止。
stimeo--toast コントローラは、極めてアクセシブルな通知キュー(WAI-ARIA ライブ領域)を実装します。通知が表示されると、自動的にリストターゲットへ追加されます。WCAG 2.2.1(調整可能な時間)に準拠するため、通知にマウスホバー(mouseenter)するか、キーボードフォーカス(focusin)が当たると自動消去タイマーが一瞬停止し、外れると再開します。Turbo Streams などによる動的 DOM 追加も、Stimulus ターゲットライフサイクルで透過的にハンドリングされます。
キーボード操作
| キー | 動作 |
|---|---|
| Esc | フォーカスされているトースト通知を即座に閉じる。 |
| Tab / Shift+Tab | トースト通知にフォーカスを当ててタイマーを一時停止させる。 |
<%# 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;
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--toast"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
list
必須
|
新しいトースト項目を追加するライブリージョンのリスト(例: <ol>)。 |
data-stimeo--toast-target="list" |
template
|
各トーストを生成するために複製する <template>。メッセージを差し込む body スロットを持つ。 |
data-stimeo--toast-target="template" |
item
|
個々のトースト(role="status"/alert)。ホバー/フォーカスで一時停止し、破棄可能で、遷移用に data-state を追跡する。 |
data-stimeo--toast-target="item" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
duration
|
自動破棄までのミリ秒。0 で自動破棄を無効化(既定 0)。 | data-stimeo--toast-duration-value |
max
|
同時に表示するトーストの最大数。超過分は古いものから削除(既定 3)。 | data-stimeo--toast-max-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
dismiss
|
トリガーを含むトーストを破棄する(reason user)。 |
stimeo--toast#dismiss |
onKeydown
|
Escape 押下時にフォーカス中のトーストを破棄する。 | stimeo--toast#onKeydown |
pause
|
ホバー/フォーカスで自動破棄タイマーを一時停止し、残り時間を記録する(WCAG 2.2.1)。 | stimeo--toast#pause |
resume
|
ホバーとフォーカスの両方が解除されたら自動破棄タイマーを再開する。 | stimeo--toast#resume |
show
|
テンプレートを複製し、param または event detail から本文を差し込み、ライブリージョンの role を設定して新しいトーストを追加する。 | stimeo--toast#show |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
dismiss
|
トースト除去時に発火。detail に item 要素と reason(user か timeout)を含む。 |
stimeo--toast:dismiss |
show
|
新しいトースト表示時に発火。detail に生成された item 要素を含む。 | stimeo--toast:show |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
role="status" |
トースト項目 | スクリーンリーダーに対し、控えめな通知(Status)として読み上げさせる(デフォルト)。 |
role="alert" |
トースト項目 | スクリーンリーダーに対し、割り込みの緊急通知(Alert)として即座に読み上げさせる。 |
data-paused="true" |
トースト項目 | マウスホバーやキーボードフォーカスにより、自動消去タイマーが一時停止中であることを示す。 |
data-state="entering | visible | leaving" |
トースト項目 | 現在のライフサイクル状態を示し、CSS トランジションとの統合を可能にする。 |