「保存しました」が読み上げられない — 動的更新をスクリーンリーダーに届ける設計 | GH Media
URLがコピーされました

「保存しました」が読み上げられない — 動的更新をスクリーンリーダーに届ける設計

URLがコピーされました
「保存しました」が読み上げられない — 動的更新をスクリーンリーダーに届ける設計

問い合わせフォームの送信ボタンを押すと、画面の右下に「送信が完了しました」というトーストが3秒だけ表示される。マウスで見ている人には何の問題もない。ところがスクリーンリーダーを使っている人の耳には、何も届かない。ボタンを押した感触はあったが、成功したのか失敗したのか分からないまま、不安になってもう一度押す。結果、同じ問い合わせが二重に送信される。

これはレアケースではありません。シングルページアプリケーション(SPA)やAJAXで画面の一部だけを書き換える作りが当たり前になった結果、「ページ遷移しないのに状態が変わる」場面が爆発的に増えました。ところがスクリーンリーダーは、もともとページ全体が切り替わることを前提に読み上げる道具です。DOMの一部がそっと書き換わっても、それを利用者に伝える経路を開発者が明示的に用意していなければ、変化はそのまま黙殺されます。

この記事では、その経路を作るための定番である aria-live リージョンの正しい設計と、2025年から提案・実験が進む新しい ariaNotify() という JavaScript API の使いどころ、そして両者の落とし穴を、受託Web制作の現場でどう設計・実装・検証するかという順で整理します。発注側の方も、自社サイトの動的な通知が「全員に届いているか」を判断する材料として読んでいただけます。

なぜ動的更新は「見える人にだけ」届くのか

スクリーンリーダーは、フォーカスのある場所か、利用者が読み進めている箇所を音声化します。ところがトーストやバリデーションメッセージ、検索結果の差し替えといった更新は、たいていフォーカスとは別の場所で、利用者の操作とは非同期に起こります。視覚的には画面のどこに何が出ても目に入りますが、音声では「いま読んでいる場所」以外の変化は原理的に拾えません。

この溝を埋めるのが、WAI-ARIA の「ライブリージョン(live region)」です。特定の要素に「ここは中身が動的に変わる領域だ」と印を付けておくと、その中のテキストが書き換わったタイミングで、スクリーンリーダーが自動的に読み上げてくれます。印の付け方は aria-live 属性で、値は主に二つです。

<!-- 控えめに伝える: 読み上げ中の内容が終わってから通知する -->
<div aria-live="polite" id="status"></div>

<!-- 即座に割り込んで伝える: 重要な警告など限定的に使う -->
<div aria-live="assertive" id="alert"></div>

polite は「いま読み上げている内容が一段落してから伝える」非割り込み型で、保存完了や件数更新など大半のケースに使います。assertive は読み上げ中の内容を中断して即座に伝える割り込み型で、MDN も「破壊的になりうるため控えめに使うこと」と注意しています。重大なエラーや、即時の対応が必要な警告に限るべきです。role="status"aria-live="polite"role="alert"aria-live="assertive" とほぼ等価のショートハンドとして使えます。

ここで重要なのは、ライブリージョンは「中身が変わったこと」をトリガに動くという点です。つまり、要素は更新前から存在していなければなりません。空の <div aria-live="polite"> を最初からページに置いておき、後からその中にテキストを差し込む——この順番を守らないと読み上げられません。

aria-live を「正しく」設置する

実装で最も多い失敗が、通知のたびにライブリージョンの要素ごと作って appendChild する作りです。これだと「新しい要素が現れた」だけで「既存リージョンの中身が変わった」とは見なされず、スクリーンリーダーによっては沈黙します。正解は、空のリージョンを最初から置いておき、テキストノードだけを差し替えることです。

<!-- 初期ロード時から空で配置しておく -->
<div id="form-status" aria-live="polite" aria-atomic="true" class="visually-hidden"></div>
// 送信完了時にテキストだけを書き換える
function announce(message) {
  const region = document.getElementById('form-status');
  // 同じ文言を連続で入れると変化と見なされないため、いったん空にする手当てを入れることもある
  region.textContent = '';
  requestAnimationFrame(() => {
    region.textContent = message;
  });
}

announce('お問い合わせを送信しました。担当者より2営業日以内にご連絡します。');

class="visually-hidden" は、視覚的には隠しつつスクリーンリーダーには読ませるための定番クラスです。display:nonevisibility:hidden で隠すとアクセシビリティツリーからも消えて読み上げられなくなるため、画面外へ追い出す実装(position:absolute; width:1px; height:1px; overflow:hidden; clip-path など)を使います。視覚的なトーストとは別に、この「音声専用の通知欄」を一つ持っておくのが堅実です。視覚的フィードバックと音声を一つのDOM要素に兼ねさせようとすると、トーストのアニメーションやタイミングに引きずられて読み上げが不安定になりがちなので、見た目と音声は経路を分けて考えるほうが結果的に崩れません。

なお、同じ文言を続けて流すと「変化なし」と判定されて読み上げられないことがあるため、いったん空にしてから入れ直す、あるいは末尾に不可視のカウンタを付ける、といった手当てを入れる場面があります。上のコード例で requestAnimationFrame を挟んでいるのは、空にした直後に同フレームで書き戻すと変更が一回分に潰れることがあるためで、一拍置いて確実に「変化した」と認識させる狙いです。

属性の aria-atomic="true" は「リージョンの一部だけ変わっても全体を読み直す」指定です。ただしここに落とし穴があります。aria-atomicaria-relevant(どの種類の変更を読むか)は、スクリーンリーダーと環境の組み合わせによって挙動が割れます。たとえば一部の環境では aria-relevant="additions" が尊重されず常に全体を読む、Android の Chrome と TalkBack の組み合わせでは aria-atomicaria-relevant を無視してイベントごとにリージョン全体を読む、といった報告があります。細かい制御に依存した設計は環境差で崩れやすいので、リージョンは「短く、一度に一つのメッセージ」を入れる単純な作りに寄せるのが安全です。動的なUIで状態を保持しながら通知する場面では、入力保持やセッションの扱いも併せて考える必要があり、その実装面はセッションタイムアウトとアクセシビリティの記事で詳しく扱っています。

新顔の ariaNotify() — 何が嬉しくて、何が危ういのか

ライブリージョンの根本的な弱点は、「DOMの変更を起点にしか発火しない」ことと、「読み上げ内容がDOMの中身に縛られる」ことです。中身を変えずに何かを知らせたい、あるいは画面に出すテキストと読み上げる文言を分けたい、といった場面では回りくどいハックが必要でした。

これを解決しようと、Microsoft Edge チームと WICG(Web Incubator Community Group)が中心になって提案しているのが ariaNotify() という命令的なJavaScript APIです。ライブリージョンを置かなくても、コードから直接「これを読み上げて」と指示できます。

// 要素または document に対して、読み上げてほしい文言を直接渡す
// priority は 'normal'(既定)と 'high' があり、
// normal は aria-live="polite"、high は assertive にほぼ相当する
document.body.ariaNotify('検索結果を24件に更新しました。', {
  priority: 'normal',
});

// 割り込み制御。先行・保留中の通知を止めて即座に伝えたい場合
saveButton.ariaNotify('保存しました。', {
  priority: 'high',
  interrupt: 'all',
});

DOMの状態と切り離して任意のタイミングで、画面表示とは独立した文言を読み上げられるのが利点です。priority に加えて interrupt(先行・保留中の通知に割り込むか)を指定でき、ライブリージョンでは表現しづらかった「割り込みの粒度」を扱えます。

ただし、ここで浮かれてはいけません。冒頭で触れた CSS-Tricks の「The Siren Song of ariaNotify()」が警告しているのは、まさにこの「便利すぎて飛びつきたくなる」ところです。重要な前提を二つ押さえてください。

第一に、標準化も実装も発展途上です。ariaNotify() は WAI-ARIA への提案段階の機能で、これまで Microsoft Edge のオリジントライアル(2025年後半まで)という限定的な形で試されてきた実験的APIです。2026年6月時点で全ブラウザ・全スクリーンリーダーで安定して使える標準とは言えず、仕様の細部は今後変わりうる前提で扱うべきです。GitHub は未対応環境向けに、内部でライブリージョンへフォールバックするポリフィルを公開しており、移行期はこれを併用するのが現実的とされます。

第二に、読み上げの保証がありませんariaNotify() は非同期APIで、呼んだ瞬間にスクリーンリーダーが読む保証も、そもそもスクリーンリーダーが起動しているかを知る手段もありません。これはライブリージョンも同じですが、「直接命令できる」見た目に反して、確実に届くわけではない点は変わらないということです。だからこそ「視覚的フィードバックの代わり」ではなく「視覚的フィードバックと並行する音声経路」として設計する原則は崩せません。

aria-liveariaNotify() の性格の違いを整理すると次のようになります。

観点aria-live リージョンariaNotify()
発火の起点DOM内のテキスト変更コードからの直接呼び出し(DOM変更不要)
読み上げ文言リージョンの中身に依存画面表示と独立して指定可能
標準化・対応状況広く実装され安定提案・実験段階(2026年6月時点)
割り込み制御polite / assertive の2段階priority + interrupt で細かく指定

結論として、2026年6月の本番案件で「主役」に据えるべきは依然として aria-live です。ariaNotify() は、ポリフィルでライブリージョンへ確実にフォールバックする前提でのみ、段階的に試す候補という位置づけが妥当です。

弊社事例 — 予約システムの「沈黙する空き枠更新」を直す

地方で複数施設を運営するサービス業の B 社では、施設予約のWebシステムをリニューアルした際、SPA化に伴ってこの問題が表面化しました。利用者が日付や人数を変えると、画面下部の空き枠リストがAJAXで差し替わる作りです。視覚的には小気味よく更新されるのですが、視覚障害のある利用者から「条件を変えても何も起きていないように感じる」「予約できたのか分からず、電話で問い合わせた」という声が、サポート経由で複数届いていました。

調べると、空き枠リストはコンポーネントごと丸ごと再生成(古いリストを削除して新しいリストを挿入)しており、ライブリージョンも毎回作り直されていました。前述のとおり、これでは「中身の変更」とは見なされず、多くの環境で沈黙します。さらに送信完了のトーストはCSSアニメーションだけの実装で、音声経路がまったくありませんでした。

施策は三つに絞りました。一つ目は、リストとは別に空の aria-live="polite" リージョンを初期表示から常設し、更新時には「空き枠を5件に更新しました」という要約文だけをそこへ流す形に変えたこと。リストそのものを読み上げさせるのではなく、「何件になったか」という結論を先に伝える設計です。二つ目は、予約確定・送信完了・エラーをそれぞれ role="status"role="alert" で明確に出し分け、エラーだけ assertive で割り込ませたこと。三つ目は、検証を実機のスクリーンリーダー(NVDA・VoiceOver・Android の TalkBack)で行い、環境差で読み上げが落ちないことを確認したことです。

結果として、「更新されたのか分からない」という問い合わせは大きく減り、二重予約による施設側のキャンセル処理の手間も目に見えて減りました。ariaNotify() は検討しましたが、当時の対応状況を踏まえ、今回は本番投入を見送り、堅実なライブリージョンで固めました。新しいAPIを追うことより、確実に全員へ届く設計を優先したわけです。こうした「誰が脱落しているか」を起点にした改善の考え方は、認知インクルージョンUXリサーチの記事とも地続きです。

「届いているか」を必ず実機で検証する

ここまでの設計は、検証して初めて意味を持ちます。ライブリージョンも ariaNotify() も、コード上は正しく見えても、実際の読み上げは環境の組み合わせで割れるからです。自動テストやLighthouseのスコアだけでは、「読み上げが実際に発火したか」までは捕まえられません。

実機検証では、最低でも次の組み合わせを通します。WindowsならNVDAとブラウザ、macOS・iOSならVoiceOver、AndroidならTalkBackです。確認するのは三点に絞ると効率的です。第一に、更新時に意図した文言が実際に読み上げられるか。第二に、polite の通知が利用者の操作を不必要に中断していないか(過剰な assertive は読書を妨げて、かえって体験を損ないます)。第三に、同じ更新が二重・三重に読み上げられていないか(リージョンの作り直しや属性の取り違えで起きがちです)。

検証の頻度を上げるコツは、開発の早い段階で「通知のためのライブリージョンを一つ、共通コンポーネントとして用意しておく」ことです。トーストやバリデーションを実装するたびに音声経路を後付けするのではなく、最初から announce() のような共通関数を通す設計にしておけば、抜け漏れが構造的に減ります。後から全画面を洗い直して音声経路を足す作業は、機能が増えるほど指数的に重くなります。共通関数に集約しておけば、将来 ariaNotify() が安定して使えるようになった際も、関数の内部実装を差し替えるだけで全画面に展開でき、ポリフィルからネイティブAPIへの移行コストも一点に閉じ込められます。新しいAPIへの備えとしても、この一点集約は効いてきます。フォント拡大やズーム時にも通知が崩れないかという観点は、フォント拡大に耐えるアクセシブルUIの記事と合わせて点検すると、動的UI全体の堅牢性が上がります。

まず確かめてほしいこと

新しい ariaNotify() は魅力的ですが、いま自社サイトでやるべきことは、最新APIの導入ではありません。フォーム送信後の「送信しました」、検索やフィルタの「件数が変わりました」、エラー表示——この三つが、スクリーンリーダーで実際に読み上げられるかを、一度だけでいいので実機で確かめてみてください。多くの場合、ここで音声が沈黙していることに気づくはずです。

そのうえで、ライブリージョンの設計に踏み込むと手戻りが大きそうなら、外部の手を借りるのが近道です。動的コンテンツの通知設計は、実装テクニックだけでなく「何を、いつ、どの優先度で伝えるか」という情報設計の問題でもあり、リサーチから実装・実機検証までを一貫して引き受けられるのが受託の強みです。SPAやAJAX更新のアクセシビリティ、スクリーンリーダー対応の検証体制づくりをご検討の際は、グリームハブのお問い合わせからお気軽にご相談ください。

Sources

無料ダウンロード

Web制作 費用・発注・集客 完全ガイド【2026年版】

費用相場・制作会社の選び方・集客戦略まで、中小企業のWeb担当者が知っておくべき全知識をPDFにまとめました。

メルマガにも登録されます。いつでも解除可能です。

URLがコピーされました

グリームハブ株式会社は、変化の激しい時代において、アイデアを形にし、人がもっと自由に、もっと創造的に生きられる世界を目指しています。

記事を書いた人

鈴木 翔

鈴木 翔

技術の可能性に魅了され、学生時代からプログラミングとデジタルアートの分野に深い関心を持つ

関連記事