なぜLLMチャットのアクセシビリティが重要なのか
Claude、ChatGPT、Copilot、Gemini のようなフルスクリーン型のLLMチャット体験がWeb全体に急速に広がっています。しかし多くのチームは「見た目が正しいか」しか確認していません。スクリーンリーダーユーザーの立場では、状況はまったく異なります。
重要なのは障害の社会モデルという考え方です。障害は個人の欠陥ではなく、環境が生み出す排除であるという視点です。階段の代わりにスロープを設置するのと同じように、デジタル環境も設計段階で排除を取り除く必要があります。LLMチャットの主要な操作はすでに明確です。
- テキストを入力して送信する
- 応答を読み、次のアクションを取る
問題は、この2つを「どう支援するか」にあります。視覚バイアスに囚まれていると、画面にだけ反応するUIを作ってしまいます。スクリーンリーダーユーザーは、LLMの応答が新たに表示されてもフォーカスが入力欄に留まったままで、新しいコンテンツが現れたこと自体を認識できません。本記事では、そのギャップを埋める実装パターンを解説します。
本記事は Smashing Magazine の根拠資料を基に、日本の開発コミュニティ向けに再構成したものです。
![]()
核心実装: ライブリージョンによる通知
最初に取り組むべきは、専用のライブリージョン領域をDOM末尾に配置し、LLMの応答が完了したタイミングで通知をトリガーすることです。各メッセージに aria-live を付与するのは典型的なアンチパターンです。不安定で、デバッグも困難になります。
<!-- DOM最下部に配置する専用ライブリージョン -->
<div id="sr-announcer" aria-live="polite" aria-atomic="true" class="sr-only"></div>
// LLM応答のストリーミングが「完了」した時点でのみ通知する
const announcer = document.getElementById('sr-announcer');
function announceMessage(text) {
// ストリーミング途中の通知は禁止。完了したテキストのみを一括で渡す
announcer.textContent = text;
}
// 応答完了コールバックの例
async function onLLMResponseComplete(fullText) {
announceMessage(fullText);
}
ローディング状態も必ず通知する
応答に時間がかかる場合、視覚的にのみローディングスピナーを回していると、スクリーンリーダーユーザーにはアプリが停止したように見えます。プロンプト送信から5秒後に「Loading」通知をトリガーし、応答完了まで5秒間隔で繰り返すパターンが実務で検証されています。
let loadingTimer = null;
function startLoadingAnnouncements() {
loadingTimer = setInterval(() => {
announcer.textContent = '応答を生成しています。しばらくお待ちください。';
}, 5000);
// 初回通知は5秒後
setTimeout(() => {
announcer.textContent = '応答を生成しています。しばらくお待ちください。';
}, 5000);
}
function stopLoadingAnnouncements(finalText) {
clearInterval(loadingTimer);
// 完了直後に通知(5秒周期を待たない)
announceMessage(finalText);
}
セマンティック見出しでメッセージを区別する
スクリーンリーダーユーザーは見出しを使って画面を走査します。各メッセージの前に視覚的に隠した見出しを配置し、「誰が発言したか」を明確にします。
<article>
<h2 class="sr-only" aria-label="Cody said" tabindex="-1">Cody said:</h2>
<p>Utility CSSはグローバルスタイルを犠牲にする代わりに、コンポーネントスタイルをより明示的にします...</p>
</article>
aria-label は見出しにフォーカスが当たった際に WCAG の Name, Role, Value の達成基準を満たすための保険です。tabindex="-1" は F6ナビゲーションでフォーカスを受け取れるようにする仕組みです。

実務上の注意点と発展的なTips
1. ランドマークは「適度に」
nav、main、form、aside で大きな領域を囲むのは良い判断です。しかし個々のメッセージを section で囲むのは避けてください。ランドマーク一覧が汚染され、スクリーンリーダーユーザーのナビゲーションがかえって苦痛になります。長いセッションほど致命的です。
2. スキップリンクと複合ウィジェット
- スキップリンク:
mainコンテンツ、プロンプト入力欄、プロフィール/設定領域へ直接ジャンプできるようにします。 - 複合ウィジェット: 会話セッション一覧は Tab 1回で全体を通過し、内部は矢印キーで移動できるようにします。Tab 50回 vs Tab 1回の差は、運動制御に困難を抱えるユーザーにとって決定的です。
Home/Endとタイプアヘッドまでサポートすると理想的です。
3. F6ナビゲーションのサポート
Windowsのスクリーンリーダーユーザーに馴染みのある F6 パターンを再現します。Webでは Ctrl+F6/Ctrl+Shift+F6、ネイティブラッパー(Electron/Tauri)では F6/Shift+F6 です。各ランドマークに id と tabindex="-1" を付与し、イベントリスナーで循環フォーカスを実装します。入力欄へ移動する際は form ではなく input に直接フォーカスを当て、最後のメッセージへ移動する際は視覚的に隠した見出しにフォーカスを当てることで、キーボードのみで応答確認と再入力のサイクルを高速に回せます。
4. 特殊コンテンツの通知戦略
ライブリージョンは純粋なテキストであるため、表やコードブロックのセマンティクスは失われます。2,376行のコードを1文字ずつ聞く人はいません。
- 表: 「パフォーマンス監査の表、4列36行、重要度別に分類」のように要約通知
- コードブロック: 「settings.json、738行」のようにファイル名/行数のみ通知
実際のメッセージ本文にはセマンティクスを適切に適用しておき、ユーザーが後から戻って精読できるようにします。ライブリージョンはあくまで「プレビュー」です。
5. ユーザー設定
- 通知の粒度: 中間推論やツール呼び出しなど副次的なアクションの通知をオフにできるように
- アニメーション無効化:
prefers-reduced-motionをデフォルトで尊重 - サウンド無効化: ネイティブ通知、バッジ、メール、アプリ内バナーなどの代替を提供
- ショートカットの再バインド: 他アプリとの競合、多言語キーボードレイアウト問題を解決
6. 拡大/ズームの検証
ブラウザのフォントサイズを大きくした際にコンテンツが切れたり、横スクロールが発生しないか必ず確認してください。LLMチャットUIは情報量の多さから小さなフォントを使う傾向があり、特に脆弱です。
ariaNotify()は魅力的に見えるかもしれませんが、スクリーンリーダーユーザーのアップグレード回避傾向とポリフィルのサポート範囲を考慮すると、当面は検証済みのライブリージョンパターンを維持する方が安全です。

まとめ: アクセシビリティは「できるか」ではなく「どれだけうまくできるか」の問題
LLMチャットUIは特別な魔法ではありません。Web技術で作られたインタラクティブな体験であり、他のWebコンテンツと同様のアクセシビリティ基準が適用されます。ただし視覚バイアスのせいで見落としやすいポイントが多いだけです。
実務適用の順序を整理すると以下の通りです。
- 専用ライブリージョン + 完了時通知 + 5秒ローディング通知
- 視覚的に隠した見出しでメッセージの所有者を明示
- ランドマークは大きな領域のみ、メッセージは
articleで - スキップリンク + 会話一覧の複合ウィジェット + F6ナビゲーション
- 表/コードは要約通知、本文にはセマンティクスを維持
- アニメーション/サウンド/ショートカットの設定を提供
最後に強調したいのは当事者性です。障害を持つユーザーを「助言の対象」ではなく「自身の経験の専門家」として扱ってください。包括的なユーザーリサーチなしでは、「基準は満たしたが使えない」成果物になりがちです。
あわせて読みたい
日本の開発現場では特にレガシーなデザインシステムとの衝突に注意してください。既存コンポーネントが aria-live を乱用している場合は、これを機に専用リージョンパターンへ統合リファクタリングする価値が十分にあります。