モーダルのフォーカストラップ実装方法|JavaScript・dialog・ARIA対応
モーダルは、現在のページを離れることなく、確認、入力、設定変更、削除、ログインなどの操作を利用者へ求めるために使われるUIです。背景のページよりも前面へ表示され、利用者はモーダル内の操作を完了するか、閉じるまで元の画面へ戻れない状態になります。そのため、見た目だけでなく、キーボード操作やスクリーンリーダー利用時にも、現在操作すべき範囲が明確でなければなりません。
モーダルを視覚的に表示しただけでは、キーボードのフォーカスが自動的に内部へ限定されるとは限りません。Tabキーを繰り返し押すと、背景にあるリンクや入力欄へフォーカスが移動し、見えていない要素を操作できてしまう場合があります。また、モーダルを開いた後もフォーカスが開くボタンに残ったままだと、スクリーンリーダー利用者はモーダルが表示されたことを把握できない可能性があります。
この問題を防ぐために使われるのが、フォーカストラップです。フォーカストラップでは、モーダルを開いた際にフォーカスを内部へ移動し、TabキーとShift+Tabキーによる移動をモーダル内で循環させます。さらに、モーダルを閉じた後は、元の操作要素へフォーカスを戻します。本記事では、フォーカストラップの役割、HTMLとARIAの構造、JavaScript実装、動的コンテンツや入れ子モーダルへの対応、アクセシビリティ検証まで詳しく解説します。
1. モーダルにおけるフォーカストラップとは
フォーカストラップを正しく実装するには、単にTabキーの動きを止める機能として捉えるのではなく、モーダルの開始から終了までを含むフォーカス管理として理解する必要があります。フォーカスを内部へ入れる処理、内部で循環させる処理、閉じた後に元の位置へ戻す処理は、それぞれ独立しているように見えて、実際には一つの操作体験として連続しています。
フォーカストラップがないモーダルでも、マウスだけを使っていると問題に気づかない場合があります。しかし、キーボードやスイッチデバイスを利用する人にとって、フォーカス位置は現在操作している場所そのものです。ここでは、フォーカストラップの仕組みと役割を明確にします。
1.1 モーダル内部へフォーカスを限定する仕組み
フォーカストラップとは、モーダルが開いている間、キーボードフォーカスがモーダルの外側へ移動しないように制御する仕組みです。利用者がTabキーを押して最後の操作可能要素へ到達した後は、背景ページのリンクへ移動するのではなく、モーダル内の最初の操作可能要素へ戻します。
Shift+Tabキーによって逆方向へ移動した場合も同様です。モーダル内の最初の操作可能要素からさらに前へ移動しようとした際は、モーダル内の最後の操作可能要素へ移します。この双方向の循環によって、利用者はモーダルの操作範囲から意図せず外へ出ることなく、必要な操作を続けられます。
1.2 フォーカス管理は開閉処理全体を含む
フォーカストラップという言葉は、Tabキーの循環処理だけを指すように使われることがあります。しかし、実務で必要なフォーカス管理には、モーダルを開いた直後の初期フォーカスと、閉じた後のフォーカス復帰も含まれます。
モーダルを開いたにもかかわらず、フォーカスが背景のボタンに残っていると、利用者は新しい操作領域へ移動できません。また、閉じた後にフォーカスがページ上部へ戻ったり、bodyへ失われたりすると、利用者は元の操作位置を探し直す必要があります。開く前、開いている間、閉じた後の三段階で設計することが重要です。
1.3 キーボード利用者の操作範囲を明確にする
マウス利用者は、モーダルの位置や背景の暗転によって、どこを操作すべきか視覚的に判断できます。一方、キーボード利用者はフォーカス表示を頼りに操作を進めるため、背景ページへフォーカスが移ると、モーダルが開いている状態と操作位置が一致しなくなります。
フォーカストラップを実装すると、視覚的な表示範囲とキーボード操作の範囲を一致させられます。モーダルが前面にある間は内部だけを操作し、閉じた後に元のページへ戻るという流れが、入力方法に関係なく一貫します。
1.4 スクリーンリーダー利用時にも必要になる
スクリーンリーダーは、現在フォーカスされている要素の名前、役割、状態などを読み上げます。モーダルが表示されてもフォーカスが内部へ移らなければ、利用者はモーダルの見出しや説明を認識できず、背景ページの続きを操作してしまう可能性があります。
ただし、フォーカストラップだけでスクリーンリーダー対応が完成するわけではありません。モーダルへ適切な役割を設定し、見出しと説明を関連付け、背景コンテンツを操作対象から外す必要があります。フォーカス制御と意味構造の両方を組み合わせることが重要です。
1.5 操作を閉じ込めること自体が目的ではない
フォーカストラップの目的は、利用者をモーダルの中へ強制的に閉じ込めることではありません。現在の操作対象を明確にし、誤って背景ページへ移動することを防ぎながら、閉じる手段を確実に提供することが目的です。
閉じるボタンが見つからない、Escapeキーで閉じられない、操作を完了するまで退出できないモーダルは、フォーカストラップが正しく動いていても使いやすいとはいえません。モーダル内へフォーカスを限定すると同時に、利用者が安全に終了できる経路を明確にします。
2. フォーカストラップがモーダルに必要な理由
モーダルは、背景ページよりも高い優先度を持つ一時的な操作領域です。視覚的には背景を暗くし、前面のパネルだけを強調できますが、キーボードフォーカスは見た目の重なりを自動的には理解しません。そのため、視覚表示と操作範囲を一致させるための明示的な制御が必要です。
フォーカストラップを実装しない場合、単に操作しにくくなるだけではなく、誤入力、誤送信、現在位置の喪失など、操作完了を妨げる問題が起こります。ここでは、フォーカストラップが必要になる具体的な理由を確認します。
2.1 背景の操作を防ぐため
モーダルが開いている間に背景ページのリンクやボタンへフォーカスが移ると、利用者は見えていない操作対象を選択できる状態になります。背景が暗転していても、EnterキーやSpaceキーを押すことで、ページ移動や送信処理が実行される可能性があります。
特に、背景に削除ボタン、購入ボタン、フォーム送信ボタンなどがある場合、意図しない操作につながります。フォーカストラップと背景の非活性化を組み合わせることで、モーダルが開いている間は背景の操作を確実に防げます。
2.2 現在位置の混乱を防ぐため
Tabキーを押している途中でフォーカス表示がモーダルから消えると、利用者は操作対象がどこへ移動したのか判断できなくなります。背景が暗く表示されている場合、フォーカスの輪郭が見えていても、暗転の下に隠れて確認しにくいことがあります。
フォーカスをモーダル内部で循環させれば、利用者は現在の操作範囲を予測できます。最後のボタンから次に進むと最初の入力欄へ戻るため、モーダルの構造を理解しやすくなり、操作中に位置を失いにくくなります。
2.3 入力途中の離脱を防ぐため
ログイン、会員登録、支払い情報の入力など、複数の入力欄を持つモーダルでは、Tabキーで順番に入力する利用者が多くいます。フォーカストラップがないと、最後の入力欄の後に背景ページへ移動し、送信ボタンへ到達できない場合があります。
利用者は入力内容を残したまま、再びモーダルへ戻る方法を探さなければなりません。フォーカス移動を内部へ限定し、入力欄から送信ボタン、閉じるボタンまで自然な順序で移動できるようにすると、入力完了までの流れを維持できます。
2.4 モーダルの優先度を操作面でも示すため
モーダルは、確認や判断を求めるために一時的に表示されます。削除確認や利用規約の同意などでは、モーダル内の選択が完了するまで、背景ページの操作を続けるべきではありません。
視覚的な重なりだけでなく、フォーカスの移動範囲もモーダルへ限定することで、利用者へ現在の優先タスクを伝えられます。ただし、不要にモーダルを多用すると作業を中断させるため、本当に優先して判断が必要な場面に限定します。
2.5 アクセシビリティ要件を満たすため
アクセシブルなモーダルでは、フォーカスが内部へ移動し、開いている間は内部に保たれ、閉じた後は適切な位置へ戻ることが求められます。これらは単なる追加機能ではなく、キーボードだけで操作を完了するために必要な挙動です。
モーダル内のすべての操作がマウスで使える場合でも、キーボードで同じ結果へ到達できなければ、一部の利用者は操作を完了できません。フォーカストラップは、入力手段による利用可否の差を減らすための重要な設計要素です。
3. フォーカストラップと類似するフォーカス制御の違い
フォーカストラップは、初期フォーカス、自動フォーカス、フォーカス復帰、背景の非活性化などと組み合わせて使われます。これらは関連していますが、目的と動作範囲が異なります。一つの処理だけを実装しても、モーダル全体のフォーカス管理は完成しません。
また、モーダル、非モーダルダイアログ、メニュー、ポップオーバーでは、必要なフォーカス制御が異なります。すべての浮動UIへ同じフォーカストラップを適用すると、利用者が背景へ戻りたい場面でも移動できなくなる可能性があります。
3.1 初期フォーカスとの違い
初期フォーカスは、モーダルを開いた直後に、どの要素へフォーカスを移すかを決める処理です。見出し、最初の入力欄、安全な操作ボタンなどが候補になります。
フォーカストラップは、初期フォーカスが移動した後の範囲を制御する仕組みです。初期フォーカスだけを設定しても、Tabキーを押せば背景へ移動できる場合があります。反対に、循環処理だけがあっても、開いた直後のフォーカスが背景に残っていれば、利用者はモーダルへ入れません。
| 比較項目 | 初期フォーカス | フォーカストラップ |
|---|---|---|
| 実行時点 | モーダルを開いた直後 | モーダルが開いている間 |
| 主な目的 | 操作開始位置を示す | 操作範囲を内部へ限定する |
| 対象 | 最初に選ぶ一要素 | モーダル内の操作可能要素全体 |
| 単独で十分か | 不十分 | 不十分 |
3.2 autofocus属性との違い
autofocus属性は、ページ表示時や対応するダイアログの表示時に、特定の入力要素へフォーカスを当てるために使えます。宣言的に指定できるため、単純なフォームでは便利です。
ただし、動的に生成される複数のモーダル、入れ子モーダル、条件によって初期位置が変わるUIでは、autofocusだけで十分に管理できない場合があります。また、autofocusはTabキーの循環を提供しないため、フォーカストラップとは役割が異なります。
| 比較項目 | autofocus | フォーカストラップ |
|---|---|---|
| 指定方法 | HTML属性 | HTML・CSS・JavaScript |
| 主な役割 | 最初のフォーカス指定 | フォーカス範囲の制限 |
| 動的条件への対応 | 限定的 | 実装により対応可能 |
| 循環処理 | 行わない | 行う |
3.3 背景のinert化との違い
inertを指定した要素は、フォーカスやクリックなどの操作対象から外れます。モーダルが開いている間に背景コンテンツへinertを付けると、背景のリンクや入力欄へフォーカスが移りにくくなります。
一方、フォーカストラップはモーダル内の最初と最後を循環させる処理です。背景を非活性化するだけでは、フォーカスがブラウザUIへ移ったり、モーダル内で適切に循環しなかったりする可能性があります。両方を組み合わせることで、より確実な操作制御になります。
| 比較項目 | inert | フォーカストラップ |
|---|---|---|
| 対象 | 背景コンテンツ | モーダル内部 |
| 主な目的 | 背景を操作不能にする | 内部でフォーカスを循環させる |
| クリック抑制 | 対応する | 単独では対応しない |
| Tab循環 | 行わない | 行う |
3.4 モーダルと非モーダルダイアログの違い
モーダルダイアログは、閉じるまで背景ページの操作を許可しないUIです。そのため、フォーカスを内部へ限定する必要があります。削除確認、ログイン、重要な設定変更などが代表例です。
非モーダルダイアログは、表示されたまま背景ページも操作できるUIです。補助ツール、情報パネル、移動可能な編集パネルなどでは、フォーカスを内部へ閉じ込めると不便になります。モーダルかどうかによって、フォーカストラップの必要性が変わります。
| 比較項目 | モーダル | 非モーダルダイアログ |
|---|---|---|
| 背景操作 | 許可しない | 許可する |
| フォーカストラップ | 原則必要 | 原則不要 |
| 閉じる操作 | 明確に必要 | 状況により必要 |
| 主な用途 | 確認、入力、同意 | 補助情報、並行作業 |
3.5 メニューやポップオーバーとの違い
メニューやポップオーバーも画面上に重なって表示されますが、常にフォーカストラップが必要とは限りません。メニューでは矢印キーによる項目移動や、外側をクリックした際の終了など、異なるキーボード操作が使われます。
小さな補足情報や選択肢を表示するポップオーバーへモーダルと同じ循環処理を入れると、背景へ戻りにくくなる場合があります。UIの役割、背景操作の可否、終了方法を確認し、モーダルとして扱うべき場合だけフォーカストラップを適用します。
4. モーダルを開くときの初期フォーカス設計
モーダルが表示された直後に、どこへフォーカスを移すかは、利用者が内容を理解し、操作を開始するうえで重要です。単純に最初のボタンへ移せばよいわけではなく、モーダルの目的、内容量、操作の危険性によって適切な位置が変わります。
初期フォーカスは、利用者の視線と読み上げ順序を急激に飛ばす可能性があります。入力を急がせるのではなく、必要な文脈を理解したうえで操作を開始できる位置を選びます。
4.1 最初の入力欄へ移す場合
ログイン、検索条件、短い登録フォームなど、モーダルを開いた主な目的が文字入力である場合は、最初の入力欄へフォーカスを移すと効率的です。利用者はモーダルを開いた後、追加のTab操作をせずに入力を開始できます。
ただし、入力前に読むべき重要な説明やエラーがある場合は、入力欄へ直接移すと内容が読み飛ばされる可能性があります。入力欄へ移すかどうかは、モーダルの目的だけでなく、操作前に理解すべき情報量も考慮して判断します。
4.2 モーダルの見出しへ移す場合
長い説明、複数の選択肢、重要な確認内容を含むモーダルでは、見出しや本文の先頭へフォーカスを移す方法があります。見出しへtabindex="-1"を指定すれば、通常のTab順序へ追加せず、JavaScriptからフォーカスできます。
見出しから読み始められるため、スクリーンリーダー利用者もモーダルの目的を理解しやすくなります。見出しへ移した後、Tabキーを押すと最初の操作要素へ進むよう、HTMLの順序を自然にします。
見出しへ初期フォーカスを移す例
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <div class="modal__panel"> <h2 id="modal-title" tabindex="-1">アカウントを削除しますか</h2>
<p> 削除後はアカウントと保存データを復元できません。 </p>
<button type="button">削除する</button> <button type="button">キャンセル</button> </div></div>const title = document.querySelector("#modal-title");
if (title instanceof HTMLElement) { title.focus();}
4.3 危険度の低いボタンへ移す場合
削除、取り消し不能な変更、支払い確定などの確認モーダルでは、初期フォーカスを危険な実行ボタンへ置くと、利用者が誤ってEnterキーを押した際に操作が確定する可能性があります。
このような場合は、「キャンセル」「戻る」など、安全な選択肢へ初期フォーカスを置く方法があります。危険な操作へ進むためには、利用者が意識的にフォーカスを移動する必要があるため、誤操作を減らせます。
4.4 閉じるボタンへ自動的に移さない判断
モーダルの右上に閉じるボタンがある場合、DOM上で最初の操作要素になることがあります。しかし、常に閉じるボタンへ初期フォーカスを置くと、利用者は本文を確認せずに終了操作から読み始めることになります。
閉じるボタンが主要な操作でない場合は、見出し、最初の入力欄、安全な選択肢などを優先します。閉じるボタンはTabキーで到達可能にし、Escapeキーでも閉じられるようにすると、終了経路を維持しながら自然な開始位置を作れます。
4.5 フォーカス可能要素がない場合
通知や短い説明だけを表示するモーダルでは、リンクやボタン以外にフォーカス可能な要素が存在しない場合があります。しかし、モーダルを閉じる方法がない状態は避けなければなりません。
少なくとも閉じるボタンを用意し、そのボタンまたはモーダル見出しへフォーカスを移します。モーダルコンテナ自体へtabindex="-1"を付けてフォーカスする方法もありますが、利用者が次に操作できる要素へ自然に移動できる構造を優先します。
5. TabキーとShift+Tabキーの循環処理
フォーカストラップの中心となるのが、Tabキーによる順方向の移動と、Shift+Tabキーによる逆方向の移動です。最後から最初へ戻す処理だけでは不十分で、最初から最後へ戻る逆方向の処理も必要です。
また、フォーカス可能な要素は、単純なbuttonやaだけではありません。無効状態、非表示状態、負のtabindexなどを考慮し、実際に利用者がTabキーで到達できる要素を対象にする必要があります。
5.1 最初と最後の要素を取得する
フォーカスを循環させるには、モーダル内部にある操作可能要素の一覧を取得し、最初と最後を特定します。一般的には、リンク、ボタン、入力欄、選択欄、tabindex="0"を持つ要素などを検索します。
ただし、disabledが付いたボタン、hidden要素、display: noneの要素、tabindex="-1"の要素は通常のTab移動対象になりません。取得した候補の中から、実際に表示され、操作可能な要素だけを残す必要があります。
5.2 最後から最初へ戻す
利用者がモーダル内の最後の操作要素にフォーカスしている状態でTabキーを押した場合、既定の動作を止めて最初の操作要素へフォーカスを移します。これにより、背景ページへ移動せず、モーダル内で操作を続けられます。
event.preventDefault()を呼ばずにfocus()だけを実行すると、ブラウザ既定の移動と競合する可能性があります。循環条件を満たした場合だけ既定動作を止め、それ以外では通常のTab順序を維持します。
5.3 最初から最後へ戻す
Shift+Tabキーでは逆方向へフォーカスが移動します。最初の操作可能要素からさらに前へ移ろうとした場合は、既定動作を止めて最後の操作可能要素へフォーカスを移します。
順方向だけを実装すると、逆方向の操作で背景へ出られてしまいます。キーボード検証では、TabキーだけでなくShift+Tabキーを繰り返し、両方向で循環することを確認します。
5.4 フォーカスが外側にある場合の補正
スクリプトの不具合、ブラウザ拡張、外部ウィジェットなどによって、モーダルが開いている間にフォーカスが外側へ移る可能性があります。Tabキーの端点だけを監視していると、外側からの移動を補正できません。
focusinイベントを監視し、モーダルが開いている間に外側へフォーカスが移った場合、内部の適切な要素へ戻す方法があります。ただし、無条件に戻すと開発者ツールやブラウザUIとの操作へ影響する可能性があるため、対象を文書内の要素に限定します。
5.5 操作可能要素が一つだけの場合
モーダル内に閉じるボタンしかない場合、最初と最後の操作要素は同じになります。この状態でTabキーまたはShift+Tabキーを押した場合も、背景へ移動しないようにする必要があります。
操作可能要素が一つの場合は、Tab方向に関係なく既定動作を止め、同じ要素へフォーカスを維持します。実装時には、要素数がゼロ、一つ、複数の三つの状態を分けて処理すると安全です。
Tabキー循環の実装例
const focusableSelector = [ "a[href]", "button:not([disabled])", "input:not([disabled]):not([type='hidden'])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])"].join(",");
function getFocusableElements(container) { return Array.from( container.querySelectorAll(focusableSelector) ).filter((element) => { return ( element instanceof HTMLElement && !element.hidden && element.offsetParent !== null ); });}
function trapFocus(event, modal) { if (event.key !== "Tab") { return; }
const elements = getFocusableElements(modal);
if (elements.length === 0) { event.preventDefault(); modal.focus(); return; }
const firstElement = elements[0]; const lastElement = elements[elements.length - 1]; const activeElement = document.activeElement;
if (elements.length === 1) { event.preventDefault(); firstElement.focus(); return; }
if (event.shiftKey && activeElement === firstElement) { event.preventDefault(); lastElement.focus(); return; }
if (!event.shiftKey && activeElement === lastElement) { event.preventDefault(); firstElement.focus(); }}
6. モーダルを閉じた後のフォーカス復帰
モーダルを閉じる処理では、表示を消すことだけに注意が向きやすいですが、フォーカスをどこへ戻すかも重要です。利用者はモーダルを開く前に、ページ内の特定のボタンやリンクを操作しています。閉じた後にその位置へ戻れなければ、操作の流れが途切れます。
特に長い一覧、商品カード、管理画面などでは、フォーカスがページ上部へ移動すると、元の項目を探し直す負担が大きくなります。閉じる理由や、元の要素がまだ存在するかどうかも考慮して復帰先を決めます。
6.1 開く前の要素を保存する
モーダルを開く直前に、document.activeElementを保存しておけば、閉じた後に同じ要素へフォーカスを戻せます。保存対象が実際のHTMLElementであることを確認し、モーダルごとに管理します。
複数のモーダルを同じ関数で扱う場合、単一のグローバル変数だけでは、入れ子表示や連続操作で値が上書きされる可能性があります。モーダルインスタンスごとに開いた要素を保持するか、スタック形式で管理します。
6.2 元の要素が削除された場合
削除確認モーダルでは、操作完了後にモーダルを開いたボタンや、そのボタンを含む一覧項目自体がDOMから削除される場合があります。その状態で保存した要素へfocus()を呼んでも、利用者が期待する位置へ戻れません。
元の要素が文書内に存在するかをdocument.contains()で確認し、存在しない場合は近くの見出し、次の一覧項目、一覧全体の操作ボタンなどへフォーカスを移します。削除後の画面状態に合わせた代替位置を設計します。
6.3 ページ遷移が発生する場合
モーダル内の操作によって別ページへ遷移する場合、元の要素へフォーカスを戻す必要はありません。新しいページの読み込み後に、そのページの見出しや主要コンテンツへ自然に移動します。
一方、単一ページアプリケーションで画面内容だけが更新される場合は、ブラウザのページ遷移が発生しないため、フォーカスが失われる可能性があります。遷移先の見出しや完了メッセージへフォーカスを移し、画面が変化したことを伝えます。
6.4 閉じ方によって復帰先を変える
キャンセルやEscapeキーで閉じた場合は、通常、モーダルを開いた要素へ戻すのが自然です。一方、作成や更新が完了し、新しい項目が追加された場合は、追加された項目や完了メッセージへ移したほうが分かりやすいことがあります。
すべての終了処理で機械的に同じ要素へ戻すのではなく、利用者が次に何を確認すべきかを考えます。ただし、予測できない場所へ移すと混乱するため、操作結果と明確に関連する位置を選びます。
6.5 フォーカス復帰の失敗を防ぐ
モーダルを非表示にした直後、DOM更新が完了する前にフォーカスを戻すと、別のスクリプトによってフォーカスが上書きされる場合があります。状態管理やアニメーション終了のタイミングも影響します。
閉じる処理の順序を統一し、イベントリスナーの解除、背景のinert解除、モーダル非表示、フォーカス復帰を明確に実行します。アニメーションを使う場合も、視覚的に消えるまでフォーカスを隠れたモーダルへ残さないよう注意します。
フォーカス復帰の例
let previouslyFocusedElement = null;
function openModal(modal) {
previouslyFocusedElement =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
modal.hidden = false;
const initialTarget =
modal.querySelector("[data-initial-focus]") ||
modal.querySelector("button, input, select, textarea, a[href]");
if (initialTarget instanceof HTMLElement) {
initialTarget.focus();
}
}
function closeModal(modal) {
modal.hidden = true;
if (
previouslyFocusedElement &&
document.contains(previouslyFocusedElement)
) {
previouslyFocusedElement.focus();
}
previouslyFocusedElement = null;
}
7. HTMLのdialog要素を使ったフォーカス管理
HTMLのdialog要素は、ダイアログやモーダルを表現するための標準要素です。showModal()で表示すると、モーダルとして扱われ、背景が非活性化されるなど、一般的なdiv実装より多くの動作をブラウザへ任せられます。
ただし、dialogを使えばすべてのアクセシビリティ対応が自動的に完成するわけではありません。初期フォーカス、閉じる手段、見出し構造、ブラウザごとの挙動、独自アニメーションなどは引き続き確認が必要です。
7.1 show()とshowModal()の違い
dialog.show()は、ダイアログを非モーダルとして表示します。背景ページは引き続き操作できるため、フォーカスを内部へ閉じ込める用途には適していません。
dialog.showModal()は、ダイアログをモーダルとして表示します。背景は操作対象から外れ、ブラウザがモーダル用の表示層へ配置します。確認や入力など、背景操作を停止する必要がある場合はこちらを使用します。
| 比較項目 | show() | showModal() |
|---|---|---|
| 表示形式 | 非モーダル | モーダル |
| 背景操作 | 可能 | 原則不可 |
| フォーカス管理 | 独自対応が多い | ブラウザ支援あり |
| 主な用途 | 補助パネル | 確認、入力、同意 |
7.2 ::backdropによる背景表示
モーダルとして開いたdialogでは、::backdrop疑似要素を使って背景の暗転やぼかしを設定できます。モーダル本体と背景を別要素としてHTMLへ追加する必要がなく、構造を簡略化できます。
背景を暗くしすぎると、元の文脈が完全に失われる場合があります。反対に透明度が低すぎると、モーダルと背景の区別がつきません。視覚的な優先度を示しながら、モーダル本文とのコントラストを確保します。
dialog::backdrop {
background: rgb(0 0 0 / 55%);
}
7.3 form method="dialog"による終了
dialog内部のフォームへmethod="dialog"を指定すると、送信時にページ通信を行わずダイアログを閉じられます。確認ボタンやキャンセルボタンの値は、returnValueとして取得できます。
単純な確認モーダルでは、JavaScriptの閉じる処理を減らせます。ただし、入力内容の検証、非同期通信、エラー表示などが必要な場合は、通常の送信処理と組み合わせて制御します。
<dialog id="confirm-dialog"> <form method="dialog"> <h2>変更を保存しますか</h2>
<button value="cancel">キャンセル</button> <button value="confirm">保存する</button> </form></dialog>
7.4 cancelイベントとEscapeキー
モーダル状態のdialogでは、Escapeキーによってcancelイベントが発生します。既定の動作ではダイアログが閉じますが、処理中で閉じてほしくない場合などはpreventDefault()で制御できます。
ただし、利用者が退出できないモーダルを作ることは慎重に判断する必要があります。入力途中の保護が目的であれば、Escapeキーを完全に無効化するのではなく、破棄確認を表示する方法が適しています。
7.5 独自フォーカストラップとの併用
現代のブラウザでは、showModal()によって背景操作やフォーカス制御の多くが提供されます。そのため、dialogへ従来のTab循環スクリプトを無条件に追加すると、ブラウザの挙動と競合する可能性があります。
標準機能で満たせる部分はブラウザへ任せ、必要な環境や特殊なUIだけを補います。使用するブラウザ範囲、ポリフィルの有無、入れ子モーダルの要件を確認したうえで、独自実装の必要性を判断します。
8. ARIA属性と意味構造の設計
フォーカストラップが正常に動いていても、モーダルの役割や名前が支援技術へ伝わらなければ、利用者は何が表示されたのか理解できません。ARIA属性は、モーダルの役割、見出し、説明、背景との関係を伝えるために使われます。
ただし、ARIA属性を追加するだけで不適切なHTML構造が修正されるわけではありません。見出し、本文、操作ボタンを自然な順序で配置し、その構造をARIAで補足します。
8.1 role="dialog"の役割
divなどの一般要素でモーダルを実装する場合は、コンテナへrole="dialog"を指定します。これにより、支援技術へ通常のコンテンツではなく、独立したダイアログ領域であることを伝えられます。
重要な警告や緊急性の高い確認にはalertdialogが使われる場合がありますが、すべての確認モーダルへ使うべきではありません。利用者の即時対応が本当に必要な場合に限定します。
8.2 aria-modal="true"の意味
aria-modal="true"は、表示されているダイアログがモーダルであり、背景コンテンツが現在の操作対象ではないことを支援技術へ伝えます。視覚的な暗転だけでは伝わらない操作範囲を意味的に示します。
ただし、aria-modal="true"を付けたにもかかわらず背景を操作できる状態は、意味と実際の挙動が一致しません。フォーカストラップ、背景の非活性化、クリック抑制を合わせて実装します。
8.3 aria-labelledbyによる名前付け
モーダルには、何の操作を行う場所なのかを示す名前が必要です。通常はモーダル内の見出しへIDを付け、コンテナのaria-labelledbyから参照します。
見出しの表示内容が「確認」「設定」のように抽象的すぎると、読み上げられても目的が分かりません。「配送先住所を変更」「アカウントを削除」のように、操作対象を含めた具体的な見出しを付けます。
8.4 aria-describedbyの使い方
短い説明や警告文をモーダルの説明として関連付ける場合は、aria-describedbyを使用できます。モーダルへフォーカスが移った際に、名前と説明が読み上げられることがあります。
長文や複雑な構造全体を一つの説明として関連付けると、大量の文章が一度に読み上げられ、操作を始めにくくなる場合があります。長いモーダルでは、見出しへフォーカスを移し、通常の読み上げ順序で内容を確認できる構造を優先します。
8.5 aria-hiddenとinertの違い
aria-hidden="true"は、対象要素をアクセシビリティツリーから隠すために使われますが、キーボードフォーカスやマウス操作を自動的に無効化するものではありません。背景へaria-hiddenだけを付けても、リンクへTab移動できる可能性があります。
inertは、フォーカス、クリック、選択などの操作を無効化します。背景の非活性化にはinertが適していますが、支援技術や対応環境も確認します。モーダル本体まで誤ってinertの祖先内へ含めないよう、DOM構造に注意します。
| 属性 | 主な役割 | フォーカス抑制 | クリック抑制 |
|---|---|---|---|
aria-hidden="true" | 支援技術から隠す | しない | しない |
inert | 操作対象から外す | する | する |
aria-modal="true" | モーダルであることを伝える | 単独ではしない | 単独ではしない |
hidden | 表示自体をなくす | する | する |
9. JavaScriptでフォーカストラップを実装する方法
divを使った独自モーダルや、複雑な状態管理を持つアプリケーションでは、JavaScriptによるフォーカス制御が必要になります。実装では、開く処理、Tab循環、Escapeキー、閉じる処理、背景の非活性化を一貫して扱うことが重要です。
機能を複数のイベントへ分散させると、イベント解除漏れや状態不一致が起こりやすくなります。モーダル単位のクラスや関数へまとめ、開始と終了を対になる処理として設計します。
9.1 モーダル状態を一か所で管理する
モーダルが開いているかどうかを、CSSクラス、hidden属性、JavaScript変数など複数の方法で別々に管理すると、表示状態とフォーカス制御が一致しなくなる可能性があります。
状態管理の基準を一つ決め、開く処理の中で表示、背景非活性化、イベント登録、初期フォーカスを実行します。閉じる処理では逆の順序を明確に実行します。
9.2 イベントリスナーを開閉時に管理する
モーダルを開くたびにkeydownイベントを追加し、閉じる際に解除しなければ、同じ処理が複数回実行されるようになります。Tabキーを一度押しただけで複数のfocus()が呼ばれ、予測できない位置へ移る場合があります。
同じ関数参照を保持し、開くときに追加、閉じるときに削除します。または、ページ全体に一つのイベントリスナーを置き、現在開いているモーダルだけを対象に処理する方法があります。
9.3 Escapeキーで閉じる
一般的なモーダルでは、Escapeキーによる終了を提供すると、キーボード利用者が閉じるボタンまで移動せずに退出できます。keydownイベントでevent.key === "Escape"を確認し、閉じる処理を呼びます。
ただし、入力途中の内容が失われる場合は、即座に閉じるのではなく破棄確認を表示します。Escapeキーを完全に無効化すると退出方法が見つかりにくくなるため、代替手段を明確にします。
9.4 背景クリックを制御する
背景の暗転領域をクリックした際にモーダルを閉じる設計は多く使われます。しかし、モーダル内部のクリックがイベントバブリングによって背景クリックとして処理されないようにする必要があります。
イベントのtargetが背景コンテナ自身である場合だけ閉じるなど、条件を明確にします。重要な入力モーダルでは、背景クリックによる誤った終了を避けるため、閉じるボタンとEscapeキーだけに限定する方法もあります。
9.5 再利用可能なクラスへまとめる
複数のモーダルがある場合、開閉処理をそれぞれ個別に書くと、フォーカス管理の仕様が少しずつ異なる状態になります。共通クラスや関数へまとめれば、初期フォーカス、Tab循環、背景制御を統一できます。
ただし、すべてのモーダルへ同じ初期フォーカスを適用するのではなく、data-initial-focusなどの指定によって個別に変更できる設計にします。危険操作、入力フォーム、説明中心のモーダルで適切な開始位置が異なるためです。
再利用可能な実装例
class FocusTrapModal { constructor(modal, backgroundRoot) { if (!(modal instanceof HTMLElement)) { throw new TypeError("modal must be an HTMLElement"); }
this.modal = modal; this.backgroundRoot = backgroundRoot; this.previouslyFocusedElement = null; this.handleKeydown = this.handleKeydown.bind(this); }
getFocusableElements() { const selector = [ "a[href]", "button:not([disabled])", "input:not([disabled]):not([type='hidden'])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])" ].join(",");
return Array.from( this.modal.querySelectorAll(selector) ).filter((element) => { return ( element instanceof HTMLElement && !element.hidden && element.offsetParent !== null ); }); }
open() { this.previouslyFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
this.modal.hidden = false; this.modal.setAttribute("aria-hidden", "false");
if (this.backgroundRoot instanceof HTMLElement) { this.backgroundRoot.inert = true; }
document.addEventListener("keydown", this.handleKeydown);
const initialTarget = this.modal.querySelector("[data-initial-focus]") || this.getFocusableElements()[0] || this.modal;
if (initialTarget instanceof HTMLElement) { initialTarget.focus(); } }
close() { document.removeEventListener("keydown", this.handleKeydown);
this.modal.hidden = true; this.modal.setAttribute("aria-hidden", "true");
if (this.backgroundRoot instanceof HTMLElement) { this.backgroundRoot.inert = false; }
if ( this.previouslyFocusedElement && document.contains(this.previouslyFocusedElement) ) { this.previouslyFocusedElement.focus(); }
this.previouslyFocusedElement = null; }
handleKeydown(event) { if (event.key === "Escape") { event.preventDefault(); this.close(); return; }
if (event.key !== "Tab") { return; }
const elements = this.getFocusableElements();
if (elements.length === 0) { event.preventDefault(); this.modal.focus(); return; }
const firstElement = elements[0]; const lastElement = elements[elements.length - 1];
if ( event.shiftKey && document.activeElement === firstElement ) { event.preventDefault(); lastElement.focus(); return; }
if ( !event.shiftKey && document.activeElement === lastElement ) { event.preventDefault(); firstElement.focus(); } }}
10. 動的コンテンツを含むモーダルへの対応
モーダルの内容は常に固定されているとは限りません。入力内容によって項目が増える、通信後にボタンが追加される、エラー時にリンクが表示されるなど、操作可能要素が開いた後に変化する場合があります。
最初に取得したフォーカス可能要素の一覧を使い続けると、新しく追加された要素をTab順序へ含められません。反対に、非表示になった要素へフォーカスを移そうとする問題も起こります。
10.1 Tab操作のたびに一覧を再取得する
動的モーダルでは、Tabキーが押された時点でフォーカス可能要素を再取得する方法が安全です。常に現在のDOM状態を参照するため、追加・削除された操作要素を反映できます。
要素数が非常に多いモーダルでは検索コストを考慮する必要がありますが、一般的なモーダル規模であれば大きな問題になりにくい方法です。正確性を優先し、必要になった時点で取得します。
10.2 非同期通信中のフォーカス
送信ボタンを押した後に読み込み状態へ変化する場合、ボタンをdisabledにすると、現在フォーカスされている要素が操作不能になります。ブラウザによってはフォーカスが失われる可能性があります。
読み込み状態を伝える領域へフォーカスを移すか、ボタンを無効化せずaria-disabledと処理中フラグで多重送信を防ぐ方法を検討します。通信完了後は、成功メッセージやエラー箇所へ適切に移動します。
10.3 エラーメッセージが追加された場合
入力エラーが発生した際は、単に赤い文字を追加するだけでなく、最初のエラー入力欄またはエラー概要へフォーカスを移します。利用者が何を修正すべきかすぐ確認できる状態にします。
エラー表示後にフォーカス可能要素の順序が変わる場合、フォーカストラップの端点も更新する必要があります。Tab操作時に一覧を再取得していれば、新しいリンクやボタンも自動的に循環対象へ含められます。
10.4 条件付き表示の要素を除外する
CSSでvisibility: hidden、display: none、hidden属性などによって隠れている要素は、フォーカス対象から除外します。見えない要素へフォーカスが移ると、利用者は現在位置を判断できません。
offsetParent !== nullだけでは、固定配置や一部の表示条件で正確に判定できない場合があります。必要に応じてgetComputedStyle()、getClientRects()、aria-hiddenなども確認し、プロジェクトの構造に合った判定を作ります。
10.5 コンテンツ更新を読み上げで伝える
モーダル内の内容が非同期で変化しても、フォーカスが別の場所にある場合、スクリーンリーダー利用者は更新に気づかない可能性があります。成功、失敗、読み込み完了などの重要な状態は、ライブリージョンを使って伝えられます。
ただし、更新のたびに大量の文章を読み上げると操作を妨げます。「保存しました」「入力内容を確認してください」のように、行動判断に必要な短い情報を伝えます。
11. 入れ子モーダルと複数モーダルの管理
一つのモーダルから別のモーダルを開く設計は、確認の確認を生み、利用者を混乱させやすいため、可能な限り避けるべきです。しかし、日付選択、補助検索、破棄確認など、実務上必要になる場合があります。
入れ子モーダルでは、フォーカス復帰先、背景の非活性化、Escapeキーの対象を階層ごとに管理しなければなりません。単一モーダル向けのグローバル変数では正しく動作しないことがあります。
11.1 モーダルをスタックで管理する
複数のモーダルが開く可能性がある場合は、現在開いているモーダルを配列のスタックとして管理します。新しいモーダルを開くたびに追加し、閉じる際は最後に追加したモーダルから取り除きます。
TabキーやEscapeキーの処理は、スタックの一番上にあるモーダルだけを対象にします。背後にあるモーダルは表示されていても、現在の操作対象にはしません。
11.2 親モーダルの状態を維持する
子モーダルを開いた際に親モーダルを完全に閉じると、入力状態やスクロール位置が失われる可能性があります。親モーダルは表示状態を維持しつつ、フォーカスと操作対象から外します。
子モーダルを閉じた後は、子を開いた親モーダル内のボタンへフォーカスを戻します。ページ背景ではなく、一つ前のモーダルが復帰先になる点が単一モーダルと異なります。
11.3 Escapeキーは最前面だけを閉じる
入れ子モーダルでEscapeキーを押した際に、すべてのモーダルが同時に閉じると、利用者は元の作業状態へ戻れません。最前面のモーダルだけを閉じるようにします。
イベントリスナーがモーダルごとに登録されている場合、同じEscapeキーイベントを複数の処理が受け取る可能性があります。イベントの対象となる最上位モーダルを一か所で判断する設計が安全です。
11.4 重なり順を階層に合わせる
子モーダルは親モーダルより前面へ表示し、子モーダル用の背景も親より上へ置く必要があります。単にz-indexを大きくするだけでなく、モーダル層ごとのルールを定義します。
ただし、入れ子が三層以上になる設計は、操作の理解とフォーカス管理が急激に難しくなります。確認を同じモーダル内の状態変更として表示できないか、画面遷移へ変更できないかを検討します。
11.5 入れ子を避ける代替設計
モーダル内の削除操作で確認が必要な場合、さらに確認モーダルを開く代わりに、元のモーダル内で確認状態へ切り替える方法があります。見出し、説明、ボタンを置き換えれば、新しいフォーカストラップを追加せずに済みます。
補助情報についても、展開領域、ツールチップ、別ページへのリンクなどで代替できる場合があります。入れ子モーダルは技術的に実装できても、利用者の操作負担が高いため、最初に情報構造を見直します。
12. モバイル端末とスクリーンリーダーでの挙動
フォーカストラップは主にTabキー操作のための機能として説明されますが、モバイル端末のスクリーンリーダーでは、スワイプ操作によって読み上げ対象を移動します。キーボードのTab循環だけを実装しても、背景コンテンツが読み上げ対象に残る場合があります。
モバイルでは画面キーボード、表示領域の変化、タッチ操作も関係します。視覚表示、フォーカス、アクセシビリティツリーの三つを一致させることが重要です。
12.1 スワイプ読み上げへの対応
モバイルスクリーンリーダーでは、左右スワイプによって次または前の要素へ移動します。背景コンテンツがアクセシビリティツリーに残っていると、モーダルの最後から背景のリンクへ読み上げが移る可能性があります。
aria-modal="true"、inert、標準のdialog要素などを適切に使い、背景を現在の操作対象から外します。Tabキーイベントだけを監視する実装では、スワイプ操作を制限できない点に注意します。
12.2 画面キーボードによる高さ変化
入力モーダルで画面キーボードが表示されると、利用可能な高さが大きく減ります。固定の高さを持つモーダルでは、送信ボタンやエラーメッセージが画面外へ移動することがあります。
モーダルパネルへ最大高さと内部スクロールを設定し、現在フォーカスされている入力欄が見える状態を維持します。上部の見出しや下部ボタンを固定する場合は、本文領域が極端に狭くならないよう確認します。
12.3 タッチ操作とフォーカスの一致
タッチで入力欄やボタンを選択した際にも、フォーカス状態が正しく更新される必要があります。視覚的にはボタンを押せても、フォーカスが背景要素へ残っていると、その後の外部キーボード操作が不自然になります。
タッチ操作だけで検証せず、スマートフォンへ外部キーボードを接続した状態も確認します。タッチ、キーボード、スクリーンリーダーの操作が途中で切り替わっても、現在位置が維持される設計が理想です。
12.4 モーダルの高さとスクロール
モバイルでモーダル全体が画面より高い場合、背景ページではなくモーダル内部をスクロールできる必要があります。背景まで同時に動くと、閉じた後のページ位置が変わり、利用者が元の場所を見失います。
背景のスクロールを停止し、モーダルパネルの本文領域へスクロールを設定します。ただし、見出しと閉じるボタンが常に見える必要があるか、本文と一緒に動くほうが自然かは内容量によって判断します。
12.5 仮想キーボード表示中の閉じる操作
画面キーボードが表示されると、モーダル上部の閉じるボタンが画面外へ押し出される場合があります。入力を終えないとモーダルを閉じられない状態は避ける必要があります。
Escapeに相当する操作がないモバイル端末では、画面上の閉じるボタンが特に重要です。キーボード表示中もアクセスできる位置に置くか、フォーム下部にもキャンセル操作を用意します。
13. アクセシブルなモーダル設計で注意する点
フォーカストラップは重要ですが、モーダル全体のアクセシビリティを単独で保証するものではありません。見出し、説明、ボタン名、エラー表示、閉じる手段、背景制御など、複数の要素が一貫して動作する必要があります。
また、モーダルを使う必要があるかという判断も重要です。通常のページや展開領域で提供できる内容をモーダルへ入れると、利用者の作業を不必要に中断する可能性があります。
13.1 明確な見出しを付ける
モーダルには、表示された理由を一文で理解できる見出しが必要です。「確認」「お知らせ」だけでは、何を確認するのか分かりません。「注文内容を確認」「保存せずに閉じますか」のように、対象と行動を含めます。
見出しは視覚的なタイトルとしてだけでなく、aria-labelledbyによる名前付けにも使います。ページ内の見出し階層と完全に連続していなくても、モーダル内部の情報構造が分かるようにします。
13.2 閉じる手段を複数用意する
一般的なモーダルでは、画面上の閉じるボタン、キャンセルボタン、Escapeキーなど、複数の終了手段を提供すると利用しやすくなります。背景クリックで閉じる場合も、背景クリックだけに依存してはいけません。
閉じるボタンはアイコンだけでなく、アクセシブルな名前を持たせます。重要な入力内容がある場合は、閉じる前に破棄確認を行い、誤って内容を失わないようにします。
13.3 ボタンの意味を具体的にする
確認モーダルで「はい」「いいえ」だけを表示すると、モーダル本文を読み返さなければ各ボタンの結果を判断できません。「削除する」「キャンセル」「変更を保存」のように、実行内容を直接示します。
危険な操作では、危険側のボタンだけを強調しすぎると誤操作につながる場合があります。色だけに頼らず、文言、配置、説明によって結果を理解できるようにします。
13.4 フォーカス表示を消さない
モーダルのデザインに合わせるためにoutline: noneを指定すると、キーボード利用者は現在位置を確認できなくなります。フォーカストラップが正常でも、フォーカス表示が見えなければ操作できません。
標準の輪郭を削除する場合は、十分に目立つ独自のフォーカススタイルへ置き換えます。背景色、ボタン色、暗転背景など、モーダル内のすべての状態で確認できるコントラストを確保します。
.modal :focus-visible { outline: 3px solid currentColor; outline-offset: 3px;}
13.5 モーダルを多用しない
モーダルは利用者の現在の作業を中断し、操作範囲を一時的に限定します。小さな説明、補足情報、単純な成功通知までモーダルにすると、閉じる操作が何度も必要になります。
ページ内展開、通知領域、別ページ、ポップオーバーなど、より中断の少ない方法で提供できないか検討します。重要な判断や、現在の文脈を保ったまま完了すべき短い操作に限定すると効果的です。
14. フォーカストラップの動作確認と不具合調査
フォーカストラップは、マウスだけで確認すると問題を見落としやすい機能です。Tabキー、Shift+Tabキー、Escapeキー、スクリーンリーダー、動的コンテンツなど、複数の操作条件で検証する必要があります。
自動テストも有効ですが、フォーカスの見え方や読み上げ順序は手動確認が欠かせません。実装時に確認項目を定型化しておくと、モーダル追加時の品質差を減らせます。
14.1 Tabキーを繰り返して確認する
モーダルを開いた後、Tabキーを繰り返し押し、すべての操作要素へDOM順に移動するか確認します。最後の要素から次に進んだ際は、最初の要素へ戻る必要があります。
背景ページのリンク、ブラウザのアドレス欄、非表示要素へ移動しないことも確認します。操作要素が一つだけの場合や、途中で要素が追加される場合も検証します。
14.2 Shift+Tabキーで逆方向を確認する
最初の要素にフォーカスした状態でShift+Tabキーを押し、最後の要素へ移動するか確認します。順方向だけ正常でも、逆方向から背景へ抜ける実装は少なくありません。
モーダルを開いた直後の初期フォーカスから逆方向へ移動した場合も確認します。見出しへtabindex="-1"で初期フォーカスを置いている場合、最初のShift+Tabの挙動を明確にします。
14.3 Escapeキーと閉じるボタンを確認する
Escapeキーで閉じる設計の場合、モーダルが閉じた後に元の操作要素へフォーカスが戻るか確認します。閉じるボタン、キャンセルボタン、背景クリックなど、すべての終了経路で復帰処理が一致しているかも重要です。
入力途中のモーダルでは、Escapeキーによる破棄確認が正しく表示されるか確認します。子モーダルがある場合は、最前面だけが閉じることを検証します。
14.4 動的状態を確認する
入力エラー、読み込み中、通信成功、通信失敗、ボタンの無効化など、モーダル内部の状態が変化した際にフォーカスが失われないか確認します。
特に、フォーカス中のボタンをDOMから削除する処理は注意が必要です。削除後に適切なメッセージや次の操作へフォーカスを移し、bodyへ失われないようにします。
14.5 不具合の症状から原因を探す
フォーカスが背景へ出る場合は、Tab循環の端点、背景のinert、動的要素の再取得を確認します。モーダルを閉じた後にページ上部へ戻る場合は、復帰先の保存やDOM内の存在確認を調べます。
フォーカスが見えない場合は、CSSのoutline、スクロール位置、固定ヘッダーとの重なりを確認します。症状を分類し、表示、意味構造、フォーカス移動、背景制御のどこに問題があるかを切り分けます。
| 症状 | 主な原因 | 確認する箇所 |
|---|---|---|
| 背景へフォーカスが出る | 循環処理不足 | 最初・最後の判定 |
| 開いても内部へ移らない | 初期フォーカス未設定 | 開く処理 |
| 閉じると位置を失う | 復帰先未保存 | activeElement |
| 見えない要素へ移る | 表示判定不足 | フォーカス対象の取得 |
| Escapeで複数閉じる | イベント重複 | モーダルスタック |
| フォーカス枠が見えない | CSSで輪郭削除 | :focus-visible |
15. 実務で安定するフォーカストラップ設計
実務のモーダルでは、単純なサンプルコードだけでは対応できない状態が発生します。非同期通信、複数モーダル、画面遷移、アニメーション、外部ライブラリなどが組み合わさるため、個別のイベント処理ではなく、共通の設計ルールが必要です。
フォーカストラップを毎回独自実装するのではなく、標準要素、信頼できるUI基盤、共通コンポーネントを活用し、プロジェクト全体で挙動を統一すると保守しやすくなります。
15.1 標準のdialogを優先的に検討する
利用対象ブラウザで問題なく使える場合は、dialogとshowModal()を優先的に検討します。背景の非活性化やモーダル表示層などをブラウザへ任せられるため、独自実装の範囲を減らせます。
ただし、既存のデザインシステム、アニメーション、サーバー側レンダリングとの相性も確認します。標準要素を使う場合でも、見出し、初期フォーカス、フォーカス復帰、閉じる手段の確認は必要です。
15.2 共通コンポーネントとして管理する
ページごとに異なるモーダル実装を作ると、あるモーダルではEscapeキーが使え、別のモーダルでは使えないといった不一致が生まれます。フォーカス管理、ARIA属性、背景制御を共通コンポーネントへまとめます。
個別モーダルでは、見出し、説明、主要操作、初期フォーカス対象などを設定できるようにします。共通化は見た目だけでなく、キーボード挙動や終了後のフォーカスまで含めて行います。
15.3 ライブラリ利用時も挙動を確認する
UIライブラリやアクセシビリティ支援ライブラリには、フォーカストラップを提供するものがあります。複雑なブラウザ差や動的要素への対応を任せられるため、独自実装より安全な場合があります。
ただし、導入しただけで要件を満たすとは限りません。初期フォーカス、復帰先、入れ子モーダル、ポータル表示、外部ウィジェットとの関係を確認します。ライブラリの既定値を理解せずに上書きすると、元のアクセシビリティ機能を壊す可能性があります。
15.4 自動テストと手動テストを組み合わせる
自動テストでは、モーダルを開いた後のdocument.activeElement、Tab循環、閉じた後の復帰先などを確認できます。回帰テストへ追加すれば、構造変更による不具合を早く検出できます。
一方、フォーカス輪郭の視認性、スクリーンリーダーの読み上げ、モバイル端末での操作感は手動確認が必要です。自動テストだけに依存せず、キーボードと支援技術による確認項目を開発工程へ組み込みます。
15.5 モーダルを開かない設計も検討する
フォーカストラップの最も確実な不具合対策は、不要なモーダルを使わないことです。単純な説明や設定変更をページ内で完了できる場合は、展開領域や通常ページのほうが操作しやすいことがあります。
モーダルを採用する前に、利用者の作業を中断する必要があるか、背景操作を停止する必要があるか、短時間で完了できる内容かを確認します。本当にモーダルが適している場面へ限定することで、フォーカス管理の複雑さと利用者の負担を同時に減らせます。
実装前の確認例
const modalChecklist = { hasClearTitle: true, hasCloseButton: true, supportsEscape: true, movesInitialFocus: true, trapsTabFocus: true, disablesBackground: true, restoresFocus: true, supportsDynamicContent: true, testedWithKeyboard: true, testedWithScreenReader: true};
const isReady = Object.values(modalChecklist).every(Boolean);
if (!isReady) { console.warn("モーダルの実装要件を再確認してください。");}
おわりに
モーダルにおけるフォーカストラップは、Tabキーを最後から最初へ戻すだけの処理ではありません。モーダルを開く前の操作要素を保存し、表示後に適切な初期位置へフォーカスを移し、開いている間は内部で循環させ、閉じた後に元の文脈へ戻すまでが一つのフォーカス管理です。
また、フォーカストラップだけでは背景のクリック操作やスクリーンリーダーの読み上げ範囲を完全に制御できません。dialog要素、aria-modal、aria-labelledby、inert、明確な閉じる操作などを組み合わせ、視覚表示、キーボード操作、アクセシビリティツリーを一致させる必要があります。
実装では、静的な操作可能要素だけを想定せず、入力エラー、非同期通信、要素の追加や削除、入れ子モーダル、画面遷移なども考慮します。TabキーとShift+Tabキー、Escapeキー、閉じるボタン、スクリーンリーダー、モバイル端末を使って検証し、すべての終了経路でフォーカスが適切に復帰することを確認します。
フォーカストラップの目的は、利用者をモーダル内へ閉じ込めることではなく、現在操作すべき範囲を明確にし、安全に操作を完了または中止できるようにすることです。標準のdialog要素や信頼できる共通コンポーネントを活用しながら、一貫性のあるアクセシブルなモーダルを設計しましょう。
EN
JP
KR