ライブリージョンの読み上げ順
「カートに追加」は aria-live=polite で今の読み上げが終わってから、エラーは role=alert(assertive)で割り込んで読まれる。帯に読み上げ中の文と待ち行列を可視化。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- カート追加・保存の結果を画面を見ずに届ける
- エラーだけ割り込みで読ませたい通知
- 連打しても順番に届く件数の更新
- 避ける場面
- 全部を assertive にする
- ライブ領域を後から DOM に足す
- 見た目のトーストだけで aria-live を付けない
- 構成要素
- 商品と操作ボタン
- polite の状態文
- alert の割り込み文
- 読み上げ中の帯
- 待ち行列
- 持っている状態
- 通常
- フォーカス
- キーボード
Tab で「カートに追加」「送信して失敗させる」へ、Enter/Space で発火。読み上げの進行は帯で確認する
- 支援技術
成功は role=status(aria-live=polite)で順番に、エラーは role=alert(assertive)で今の読み上げを捨てて割り込む。要素は最初から置き中身だけ差し替える
- 動き
帯の進行バーは文字数×80ms(最低 1.4s)の linear。読み上げにかかる時間の可視化。reduced-motion では伸ばさず満杯で静止
デザインの要点
- 成功・件数の更新は polite。ユーザーが今聞いている文を遮らないので、連打しても順番に届く
- エラーだけ assertive(role=alert)。割り込みは今の読み上げを捨てるので、本当に止めるべき事だけに使う
- ライブリージョンの要素は最初から DOM に置いておき、中身のテキストだけ差し替える。後から要素ごと足すと読まれないことがある
- 見た目の通知(トースト)とライブリージョンは別物。画面に出ていても aria-live が無ければ読み上げは起きない
使いどころ
画面を見ていない・見ていられない人へ、操作の結果だけを届けたいときに使う。カートの点数、保存の完了、絞り込み後の件数など、フォーカスを奪うほどではないが伝わらないと困る変化が対象。逆に、判断や操作を必ず求める内容(確認・再入力)はライブリージョンではなく、ダイアログやフォーカス移動で伝える。読み上げは流れて消えるので、後から読み返せる必要がある情報にも向かない。秒単位で更新され続けるカウンタをそのまま polite へ流すと読み上げが詰まり続けるため、間引くか、途中経過を伝えず「完了」だけを届ける形にする。
仕組み
成功や件数の更新は aria-live="polite"(見本は role="status")の領域へ、エラーは role="alert"(暗黙に assertive)の領域へテキストを入れる。polite は今読んでいる文が終わるのを待ってから読まれ、assertive は現在の読み上げを捨てて割り込む。見本はこの違いを可視化するために読み上げを待ち行列としてモデル化しており、polite の発話は queue へ push されて順に「読み上げ中」の帯へ流れる。assertive が来ると待ち行列を丸ごと捨ててその1件だけに置き換える——これが「割り込みは今の読み上げを捨てる」の再現になっている。進行バーは文字数×80ms(最低 1.4 秒)で読み上げ時間を表し、prefers-reduced-motion では伸ばさず満杯で静止させて、時間の意味は帯の文言側が担う。
実装の注意
最大の失敗は全部を assertive にすること。割り込みは今の読み上げを捨てるので、多用すると直前の文が最後まで聞けない画面になる。次に多いのが、ライブリージョンの要素を後から DOM へ追加する実装で、支援技術は要素が存在する状態で中身が変わったことを検知するため、要素ごと足すと初回が読まれないことがある。空の要素を最初から置き、テキストだけ差し替えるのが確実。見た目のトーストを出しただけで aria-live を付け忘れる取り違えもよくあり、画面に出ていることと読み上げられることは別物。逆に同じ文言を続けて入れると「変化なし」と判断されて読まれないことがあるので、件数のように毎回違う値を含める。
アクセシビリティ
aria-live の値は off/polite/assertive の3段で、role="status" は polite、role="alert" は assertive の既定を持つ。role を使うと役割も同時に伝わるので、素の aria-live より意図が明確になる。aria-atomic="true" を付けると領域全体を読み直し、既定の false では変わった部分だけが読まれる——「カートに 3 点入っています」のように文として意味を持つ場合は atomic を真にするとよい(見本は領域ごと文を差し替えるので付けていない)。ライブリージョンは視覚的に隠してよいが、display:none では変化が検知されないので、位置を画面外へ逃がすいわゆる sr-only の手法を使う。読み上げのタイミングは支援技術とブラウザの組み合わせで差があるため、重要な情報をライブリージョンだけに載せない。
調整して使う
まず粒度。連続する操作をそのまま読ませるか、一定時間まとめて「3 点追加しました」と1回にするかで、うるささが大きく変わる。次に文面で、状態を述べる(「カートに 3 点入っています」)か、行った操作を述べる(「カートに追加しました」)かを選ぶ。前者は何度聞いても現在値が分かるので、連打されうる操作では有利。3つめは assertive を使う条件で、見本は通信の失敗に限っている。ここを「バリデーションエラー全般」まで広げると割り込みが増えるので、入力の誤りは項目側の aria-describedby とエラーサマリに任せ、割り込みは本当に手を止めてほしい事象だけに残す。
コード(コピーして使えます)
<div class="lv-wrap">
<div class="lv-product">
<div class="lv-thumb" aria-hidden="true"></div>
<div class="lv-info"><strong>ひのきのまな板 M</strong><span>¥3,980・在庫あり</span></div>
</div>
<div class="lv-actions">
<button type="button" id="lv-add" class="lv-primary">カートに追加</button>
<button type="button" id="lv-fail" class="lv-secondary">送信して失敗させる</button>
</div>
<p class="lv-status" role="status" aria-live="polite" id="lv-polite"></p>
<p class="lv-alert" role="alert" id="lv-alert"></p>
<div class="lv-band">
<div class="lv-row"><span class="lv-tag">読み上げ中</span><span id="lv-now">(待機中)</span></div>
<div class="lv-progress" aria-hidden="true"><span id="lv-bar"></span></div>
<div class="lv-row"><span class="lv-tag">待ち行列</span><span id="lv-queue">—</span></div>
</div>
</div>