フォーム送信後の「保存しました」、バリデーションエラーの「メールアドレスを入力してください」。こうしたトースト通知やフォームエラーをスクリーンリーダーに読み上げさせる定番は、画面の隅に置いた空の aria-live div を使い回す方法でした。表示のたびに textContent を書き換えれば読み上げられる。理屈は単純ですが、実務では「二重に読まれた」「連続通知を取りこぼした」「フォーカスと読み上げの順番がちぐはぐになった」という職人芸の調整がつきものです。Safari 27 はこの領域に ariaNotify API への対応を持ち込みました。DOM に読み上げ専用の空要素を置かなくても、要素から直接通知を投げられるようになる変更です。この記事では従来の live-region 使い回しと ariaNotify の最小実装、そして現実的な併用法を整理します。なお支援技術の対応は執筆時点で流動的なため、数値は目安として読み、導入前には手元の環境での実測をお願いします。
Before: live-region使い回しの定型コード
従来の実装はシンプルです。HTML に視覚的には隠したライブリージョンをひとつ置き、通知が出るたびに JavaScript で中身を差し替えます。role="status" は暗黙に aria-live="polite" を持つため、トーストのような「いまの操作を邪魔せず伝えたい」通知に向いています。エラーなど割り込みが必要な場合は role="alert"(暗黙に assertive)を使い分けます。
<!-- 読み上げ専用の空リージョンを使い回す -->
<div id="toast" role="status" aria-live="polite"></div>
<div id="form-error" role="alert"></div>
<button id="save-btn" type="button">保存する</button>const toastRegion = document.getElementById('toast');
function notifyToast(message) {
// 同一文の再通知に備えて一度空にするのが定番の職人芸
toastRegion.textContent = '';
// リージョンの再計算を確実にするための短い遅延
requestAnimationFrame(() => {
setTimeout(() => {
toastRegion.textContent = message;
}, 30);
});
}
document.getElementById('save-btn').addEventListener('click', () => {
// 本来は保存処理の成否を見て分岐する
notifyToast('下書きを保存しました');
});このパターンの悩みは三つあります。一つ目は二重読みです。可視トースト自体にも role="status" を付けると、使い回しリージョンと可視トーストの両方が読まれて「保存しました、保存しました」と繰り返します。二つ目は取りこぼしです。短い間隔で通知を連打すると、クリアと書き込みのタイミングが競合して後発が読まれません。三つ目は関連付けの弱さです。読み上げ専用 div は通知元のフォーム項目と DOM 上は無関係なため、「どの入力欄のエラーか」が支援技術に伝わりにくく、aria-describedby やフォーカス移動で別途補う必要があります。動くには動くのですが、空 div の掃除と遅延の調整が属人化しやすいのが実情です。
可視トーストに直接 role="status" を付ければ空 div は不要なのでは?→ 単発の通知ならそれで十分な場合も多いです。空 div の使い回しが効くのは「通知の見た目と読み上げのタイミングを分けたい」「表示アニメーションと読み上げを別制御したい」といったケースです。どちらにせよ、連続通知のキューイングは自前実装になります。
After: ariaNotifyの最小実装
ariaNotify は、要素に対して「この要素に関する通知を支援技術に送る」ための Element のメソッドです。基本形は素直で、通知したいメッセージを渡して呼び出すだけです。読み上げ専用の空 div を HTML に置く必要がなく、通知元(保存ボタンやフォーム項目など)の要素から直接投げられる点が従来との最大の違いです。関連する要素が通知の起点になるため、「どの文脈の通知か」が保たれやすい設計です。
const saveButton = document.getElementById('save-btn');
saveButton.addEventListener('click', () => {
// 基本形: メッセージを要素から直接通知する
if ('ariaNotify' in saveButton) {
saveButton.ariaNotify('下書きを保存しました');
} else {
// 非対応ブラウザー向けの従来処理(live-region 使い回し)にフォールバックする
notifyToast('下書きを保存しました');
}
});フォームエラーの例も考え方は同じです。エラーになった入力欄の要素から通知を投げることで、通知と入力欄の対応関係が自然に保たれます。従来のように画面隅の汎用リージョンへ転送するひと手間が要りません。
const emailInput = document.getElementById('email');
function notifyFieldError(input, message) {
if ('ariaNotify' in input) {
input.ariaNotify(message);
} else {
const errorRegion = document.getElementById('form-error');
errorRegion.textContent = '';
requestAnimationFrame(() => {
setTimeout(() => {
errorRegion.textContent = message;
}, 30);
});
}
}
// バリデーション失敗時
notifyFieldError(emailInput, 'メールアドレスを入力してください');読み上げの緊急度(従来の polite / assertive に相当する概念)については、仕様やブラウザーの実装で差異があり得るため、ここでは断定しません。執筆時点では「優先度のような指定ができる余地がある」程度の理解に留め、まずは引数ひとつの基本形と機能検出("ariaNotify" in element)で使うのが安全です。割り込みが必要なエラー通知は、後述するように当面は role="alert" のリージョンを併用し、要実測で切り替える二段構えをおすすめします。細かい API シグネチャは MDN のリファレンスで最新を確認してください。
移行の第一歩は「可視トーストは見た目に専念、読み上げは要素起点に」が目安です。既存の空 div をいきなり消すのではなく、対応ブラウザーでは ariaNotify を優先し、非対応では従来路に落とす漸進強化にすると、検証しながらの置き換えがしやすくなります。
Chromeではどうなの?
執筆時点のブラウザー対応の目安は下表の通りです。Chrome は 141 以降で部分対応、Firefox は 150 以降で対応、Safari は 27 以降で対応とされています。詳細なバージョンやプラットフォーム差は変わり得るため、最新は caniuse の ariaNotify 対応表で確認してください。
ブラウザー | 対応の目安 | 備考 |
|---|---|---|
Chrome | 141+ 部分対応 | 執筆時点で挙動差があり得る。要実測 |
Firefox | 150+ 対応 | バージョン・OS で差があり得る。要実測 |
Safari | 27+ 対応 | 本記事のきっかけ。macOS / iOS で要実測 |
ブラウザーが揃いかけている一方で、正直な結論としてはスクリーンリーダー側がまだらです。執筆時点の手触りとして NVDA は比較的素直に読む、JAWS や VoiceOver は条件次第で差が出る、といった報告が見られますが、組み合わせ(ブラウザー × 支援技術 × OS)で結果が変わるため断定はできません。つまり「API が呼べること」と「ユーザーに届くこと」は別の話です。だからこそ正解は、live-region を捨てることではなくlive-region 併用の漸進強化です。対応環境では ariaNotify の素直な通知を活かし、非対応や読み上げが不安定な環境では従来リージョンが拾う構成にします。
// 通知ヘルパー: ariaNotify 優先、なければ live-region に落とす
const liveRegion = document.getElementById('toast');
function announce(sourceElement, message) {
// 機能検出が第一関門。メソッド名の存在だけを見る
if (sourceElement && 'ariaNotify' in sourceElement) {
sourceElement.ariaNotify(message);
return;
}
// フォールバック: 従来の使い回しリージョン
liveRegion.textContent = '';
requestAnimationFrame(() => {
setTimeout(() => {
liveRegion.textContent = message;
}, 30);
});
}
// 使い方: 通知元を第一引数に渡す
// announce(saveButton, '下書きを保存しました');
// announce(emailInput, 'メールアドレスを入力してください');検証のコツは、通知を「読めた / 読めなかった」の二値ではなく「どの経路で読めたか」で記録することです。ariaNotify 路と live-region 路のどちらが読んだのかをコンソールやテストメモに残すと、支援技術ごとの差が見えてきます。自動テストで API の有無は検出できても、実際に読まれたかは人手の確認が必要です。リリース前には NVDA・JAWS・VoiceOver の代表的な組み合わせで、連続通知とエラー割り込みの二系統を必ず実測してください。
いずれ live-region は不要になるの?→ 短期的には不要になりません。ariaNotify が届かない組み合わせが残る限り、フォールバックのリージョンは保険として残ります。将来支援技術側が追いつけば、汎用の空 div を減らして要素起点の通知に寄せていく流れになると考えられますが、執筆時点では併用が堅実です。
まとめ
空の aria-live div 使い回しは動きますが、二重読みと取りこぼしの調整が属人化しがちです。Safari 27 の ariaNotify 対応で、要素起点の素直な通知という選択肢が増えました。支援技術側の対応がまだらな執筆時点では、機能検出を添えた live-region 併用の漸進強化が現実解です。






