アナウンサー
stimeo--announcer
画面の変化をスクリーンリーダーへ伝える共通の窓口。ページに 1 つ置けば全部品が使えます。
画面が変わったことを、スクリーンリーダーへ伝えるための共通の窓口です。ページに 1 つ置けば、ほかの部品はそれぞれ読み上げ用の領域を持たずに済みます。伝え方は 2 通りで、マークアップから直接頼む方法と、任意の JavaScript から文言を送る方法があります。Turbo で中身が差し替わったときや、非同期の結果を知らせるときに使えます。読み上げのためにフォーカスを動かすことはありません。同じ文言をもう一度送れば、もう一度読み上げられます。伝え終わった文言は少ししてから消えるので、あとから読み返したときに古い知らせが残りません。
実行中
ライブリージョンは視覚的に隠れているため、スクリーンリーダーが各メッセージを読み上げ、下のトランスクリプトが目で見える形に複製します。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Markup for the live-region announcer demo.
The library owns one pair of polite/assertive live regions; triggers announce a
message into the matching region via the announce action param (attribute-only).
The regions are visually hidden (screen-reader only), so demo.js mirrors each
announcement into a visible transcript for sighted viewers. %>
<%
polite_msg = t("components.announcer.demo.polite_message")
alert_msg = t("components.announcer.demo.assertive_message")
%>
<%# The controller wraps the whole demo so the attribute-only trigger buttons sit
within its scope — a Stimulus action binds to a controller on the element or an
ancestor, so triggers placed outside the controller element never fire. %>
<div class="announcer-demo" data-controller="stimeo--announcer">
<div class="announcer-demo__controls">
<button class="demo-trigger" type="button"
data-action="click->stimeo--announcer#announce"
data-stimeo--announcer-message-param="<%= polite_msg %>">
<%= t("components.announcer.demo.announce_polite") %>
</button>
<button class="demo-trigger" type="button"
data-action="click->stimeo--announcer#announce"
data-stimeo--announcer-message-param="<%= alert_msg %>"
data-stimeo--announcer-assertive-param="true">
<%= t("components.announcer.demo.announce_assertive") %>
</button>
</div>
<%# Place once per page. The consumer (this Playground) owns the visually-hidden
styling via .visually-hidden; the library only writes the message text. %>
<div class="visually-hidden" data-label="<%= t('components.announcer.demo.label_polite') %>"
data-stimeo--announcer-target="polite" aria-live="polite" aria-atomic="true"></div>
<div class="visually-hidden" data-label="<%= t('components.announcer.demo.label_assertive') %>"
data-stimeo--announcer-target="assertive" aria-live="assertive" aria-atomic="true"></div>
<p class="announcer-demo__note"><%= t("components.announcer.demo.note") %></p>
<%# Visible echo of what was announced (the live regions themselves are hidden). %>
<ul class="announcer-log" data-announcer-log aria-hidden="true"></ul>
</div>
/*
* Presentation-only styles for the live-region announcer demo.
* The library only writes message text into the regions; this CSS owns the
* visually-hidden ("sr-only") treatment and the visible transcript styling.
*/
.announcer-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.announcer-demo__controls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* The live regions use the shared `.visually-hidden` base primitive (kept in the
* a11y tree, removed from view) — no per-demo duplication. */
.announcer-demo__note {
margin: 0;
font-size: 0.85rem;
color: var(--color-text-muted);
}
.announcer-log {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.announcer-log li {
padding: 0.4rem 0.6rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.9rem;
font-family: ui-monospace, monospace;
}
// Consumer-side JS for the live-region announcer demo (demo-only).
// The live regions are visually hidden (screen-reader only), so this mirror echoes
// each announcement into a visible transcript for sighted viewers. It also shows
// the programmatic path: any code can announce by dispatching the custom event,
// e.g. window.dispatchEvent(new CustomEvent("stimeo--announcer:announce",
// { detail: { message: "Saved", assertive: false } })).
const log = document.querySelector("[data-announcer-log]");
const regions = document.querySelectorAll("[data-stimeo--announcer-target]");
regions.forEach((region) => {
const observer = new MutationObserver(() => {
const text = (region.textContent || "").trim();
if (!text || !log) return;
// Prefer the author-localized label (data-label) over the raw aria-live token.
const level = region.getAttribute("data-label") || region.getAttribute("aria-live");
const item = document.createElement("li");
item.textContent = `[${level}] ${text}`;
log.prepend(item);
});
observer.observe(region, { childList: true, characterData: true, subtree: true });
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--announcer"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
polite
|
緊急でないメッセージ用の aria-live="polite" リージョン。無ければ生成。 |
data-stimeo--announcer-target="polite" |
assertive
|
緊急メッセージ用の aria-live="assertive" リージョン。無ければ生成。 |
data-stimeo--announcer-target="assertive" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
clearAfter
|
リージョンのテキストを消すまでのミリ秒。0 で無効(既定 1000)。 | data-stimeo--announcer-clear-after-value |
dedupeReannounce
|
同一文言をクリア→再設定で再読み上げするか(既定 true)。 |
data-stimeo--announcer-dedupe-reannounce-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
announce
|
アクションパラメータ(message・任意の assertive)かイベント detail からメッセージを通知する。 |
stimeo--announcer#announce |