モーダルダイアログとは、ページの手前に表示され、応答が終わるまで背景の操作を一時的に止める画面領域です。
削除の確認や短い入力など、利用者にその場で判断してもらう必要がある場面に向いています。
しかし、見た目だけを重ねた実装では、キーボードの移動先が背景へ抜けたり、閉じた後に操作位置を見失ったりします。
スクリーンリーダーにもダイアログの名前や目的が伝わらなければ、何が開いたのか判断できません。
この記事では、モーダルを開く前、開いている間、閉じた後の順に、必要な設計と実装を整理します。
Webアクセシビリティの基本と改善の優先順位を先に確認しておくと、モーダルを単独の部品ではなく、ページ全体の操作設計として捉えやすくなります。
モーダルで起きやすい四つの問題
フォーカスとは、キーボード入力を受け取る要素を示す現在位置です。
モーダルでは、この位置と背景の状態を同時に管理する必要があります。
| 問題 | 利用者に起きること | 設計上の対応 |
|---|---|---|
| 開いてもフォーカスが移らない | 画面は変わったのに、キーボード操作は背景に残る | ダイアログ内の適切な要素へフォーカスを移す |
| Tabキーで背景へ移動できる | 見えていない要素や操作できないはずの要素へ到達する | ダイアログ内でTabキーの移動を循環させ、背景を操作不能にする |
| 閉じる手段が分からない | マウスを使わない利用者が処理を中断できない | 画面上の閉じるボタンとEscapeキーを用意する |
| 閉じた後の位置を失う | ページの先頭などへ移動し、作業を再開しにくい | 原則として、モーダルを開いた要素へフォーカスを戻す |
フォーカストラップは、モーダルが開いている間だけTabキーの移動先をダイアログ内に保つ仕組みです。
一方、閉じるボタンもEscapeキーも機能せず、キーボードだけでは退出できない状態は、アクセシビリティ上の「キーボードトラップ」です。
似た言葉ですが、前者は操作範囲を明確にする設計であり、後者は利用者を閉じ込める不具合です。
開閉の流れで必要な挙動を決める
モーダルの実装は、ARIA属性を付ける作業だけでは完結しません。
次の三段階を一つの流れとして設計します。
- 開くとき:現在のフォーカス位置を保存し、モーダルを表示してから、内容に合った要素へフォーカスを移します。
- 開いている間:背景を操作不能にし、TabキーとShift + Tabキーの移動をダイアログ内で循環させます。
Escapeキーと画面上のボタンで閉じられるようにします。 - 閉じるとき:モーダルを非表示にし、背景を再び操作可能にして、保存した要素へフォーカスを戻します。
元の要素が削除済みなら、次の操作として自然な要素を戻り先にします。
初期フォーカスは内容に合わせて選ぶ
「最初のボタンへ移す」と固定すると、内容によっては読み始めを飛ばしてしまいます。
初期フォーカスは、利用者が最初に理解または操作すべき対象から選びます。
- 削除など取り消しにくい操作では、誤操作を避けるため「キャンセル」のような影響の小さいボタンを候補にします。
- 説明が長い場合は、冒頭の見出しに
tabindex="-1"を付け、そこへフォーカスを移す方法があります。 - 短い通知で選択肢が一つだけなら、その主要ボタンを候補にできます。
キーボード操作全般の設計は、キーボード操作とフォーカス表示の整え方でも詳しく確認できます。
WCAG 2.2とARIAをどう対応づけるか
WCAG 2.2をモーダルに適用すると、複数の達成基準が関係します。
フォーカス管理の全体を一つの達成基準だけで説明せず、次のように役割を分けて捉えると実装を確認しやすくなります。
| 確認すること | 関係する主な達成基準 |
|---|---|
| 開く、操作する、閉じる機能をキーボードで利用できる | 2.1.1 キーボード |
| 標準的な操作でモーダルから退出できる | 2.1.2 キーボードトラップなし |
| フォーカスが意味と操作を保つ順序で移動する | 2.4.3 フォーカス順序 |
| 現在のフォーカス位置を視覚的に確認できる | 2.4.7 フォーカスの可視化 |
| ダイアログの名前、役割、状態を支援技術へ伝える | 4.1.2 名前、役割、値 |
ARIAは、HTMLだけでは十分に伝わらない役割や関係を支援技術へ知らせるために使います。
ただし、aria-modal="true" を付けても、背景のクリックやキーボード操作が自動で止まるわけではありません。
実際の操作制限とフォーカス管理を実装したうえで、状態をARIAで伝えます。
ARIAの基本と誤用しやすい箇所は、WAI-ARIAで役割、状態、操作を伝える方法で整理しています。
HTMLの基本構造
次の例では、背景となる領域とダイアログを兄弟要素にしています。
この配置なら、背景だけを inert にしてもダイアログ自体は操作可能なままです。
<div id="page-content">
<button type="button" id="open-dialog" aria-haspopup="dialog">
削除の確認を開く
</button>
</div>
<div
id="confirm-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
tabindex="-1"
hidden
>
<h2 id="dialog-title">項目を削除しますか</h2>
<p id="dialog-description">
この操作を続けるか、キャンセルするかを選んでください。
</p>
<button type="button" id="cancel-dialog">キャンセル</button>
<button type="button" id="confirm-delete">削除する</button>
</div>
| 属性 | 役割 |
|---|---|
role="dialog" |
要素がダイアログであることを支援技術へ伝える |
aria-modal="true" |
表示中はダイアログ外を利用できない状態であることを伝える |
aria-labelledby |
画面に見えている見出しをダイアログの名前として関連付ける |
aria-describedby |
短く単純な説明をダイアログの説明として関連付ける |
hidden |
閉じている間はダイアログを表示しない |
aria-describedby は必須ではありません。
複数の段落、箇条書き、表を含む長い内容を一続きの説明として関連付けると、構造を追いにくくなる場合があります。
そのようなダイアログでは、見出しへフォーカスを移し、利用者が内部の構造を順に読めるようにします。
フォーカス管理の実装例
次のJavaScriptは、開く前のフォーカスを保存し、背景を操作不能にし、Tabキーの境界だけを循環させます。
簡略化した例ですが、ボタンだけを数えていた元の実装と異なり、リンクや入力欄なども候補に含めます。
const background = document.querySelector('#page-content');
const openButton = document.querySelector('#open-dialog');
const dialog = document.querySelector('#confirm-dialog');
const cancelButton = document.querySelector('#cancel-dialog');
const confirmButton = document.querySelector('#confirm-delete');
let returnFocus = null;
const focusableSelector = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[contenteditable="true"]',
'[tabindex]:not([tabindex="-1"])'
].join(',');
function getFocusableItems() {
return [...dialog.querySelectorAll(focusableSelector)].filter(
(element) => !element.hasAttribute('hidden') && element.getClientRects().length > 0
);
}
function openDialog() {
returnFocus = document.activeElement;
dialog.hidden = false;
background.inert = true;
cancelButton.focus();
}
function closeDialog() {
dialog.hidden = true;
background.inert = false;
if (returnFocus && returnFocus.isConnected) {
returnFocus.focus();
}
}
openButton.addEventListener('click', openDialog);
cancelButton.addEventListener('click', closeDialog);
confirmButton.addEventListener('click', () => {
// ここで削除処理を行い、完了後に閉じる。
closeDialog();
});
dialog.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closeDialog();
return;
}
if (event.key !== 'Tab') return;
const items = getFocusableItems();
if (items.length === 0) {
event.preventDefault();
dialog.focus();
return;
}
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
実際のコンポーネントでは、表示途中の要素、独自ウィジェット、入れ子のダイアログ、処理中の無効化も考慮します。
利用しているUIライブラリに検証済みのダイアログ部品がある場合は、その仕様と既知の制約を確認してから採用します。
閉じ方と読み上げを過不足なく設計する
画面上のボタンを必ず用意する
Escapeキーは有用ですが、すべての利用者がショートカットを知っているとは限りません。
「閉じる」または「キャンセル」と読めるボタンをダイアログ内に置きます。
アイコンだけのボタンを使う場合は、支援技術へ伝わる名前も必要です。
背景のクリックだけに依存しない
背景をクリックして閉じる挙動を追加する場合も、それを唯一の退出手段にはしません。
取り消しにくい操作や入力途中の内容がある場面では、意図しないクリックで閉じる設計が適切かも検討します。
通常のモーダルにライブリージョンを足しすぎない
ダイアログの名前、役割、フォーカス移動を正しく実装することが先です。
モーダルを開いたという理由だけで aria-live を追加すると、ダイアログの通知と重なって読み上げが冗長になる場合があります。
ライブリージョンは、フォーカスを動かさずに処理結果や更新状態を知らせる必要がある場面で使い分けます。
見た目と画面サイズへの配慮
- キーボードフォーカスを輪郭や背景色の変化で確認できるようにし、ブラウザ標準の表示を理由なく消さない。
- 内容が画面に収まらない場合は、見出しや操作ボタンを見失わずに内部をスクロールできるようにする。
- 画面を拡大しても、閉じるボタンや主要操作が表示領域の外へ固定されないようにする。
- 背景を暗くするだけで状態を伝えず、見出し、配置、フォーカス移動でもダイアログが開いたことを理解できるようにする。
- モバイルでは、ソフトウェアキーボードの表示後も入力欄とエラーメッセージを確認できるかを試す。
用途ごとにモーダルが必要か判断する
| 用途 | 判断の目安 | 主な確認点 |
|---|---|---|
| 削除の確認 | 短い判断を求めるため、モーダルにしやすい | 影響の小さい操作への初期フォーカス、明確な対象名、退出手段 |
| 会員登録 | 入力項目が多いなら、独立したページのほうが読み進めやすい場合がある | ラベル、説明、エラー、入力内容の保持、画面内スクロール |
| Cookie同意バナー | バナーだからといって常にモーダルとは限らない | 背景を本当に操作不能にする仕様か、同意と拒否をキーボードで選べるか |
| 簡単な補足説明 | 背景の作業を止める必要がなければ、非モーダルの表示も検討する | 閉じ方、表示元との関係、フォーカスを移す必要性 |
モーダルは目立たせるための装飾ではありません。
背景の操作を中断させる必要があるかを先に判断し、必要な場合だけ採用すると、利用者の作業を妨げにくくなります。
公開前の確認手順
- マウスを使わず、開く、内容を操作する、閉じるという一連の動作を確認する。
- TabキーとShift + Tabキーで、フォーカスがダイアログ内を意味のある順序で循環するか確認する。
- Escapeキーと画面上のボタンの両方で閉じられるか確認する。
- 表示中に背景のリンク、ボタン、入力欄へフォーカスやポインター操作が移らないか確認する。
- 閉じた後、元の操作位置または次の自然な操作位置へフォーカスが戻るか確認する。
- スクリーンリーダーで、ダイアログの名前、説明、ボタン名が過不足なく伝わるか確認する。
- 画面拡大、狭い画面、内容が長い場合でも、見出しと閉じる手段へ到達できるか確認する。
モーダル設計のチェックリスト
- モーダルを使う理由が、背景の作業を一時的に止める必要と一致している。
- WordPressの投稿タイトルとは別に、ダイアログ自身の内容を表す見出しがある。
- ダイアログに支援技術が認識できる名前と役割がある。
- 開くときの初期フォーカスと、閉じた後の戻り先が決まっている。
- 背景は見た目だけでなく、キーボードとポインターでも操作不能になる。
- Tabキーの移動が循環し、Escapeキーと画面上のボタンで退出できる。
- フォーカス位置が見え、拡大時やモバイルでも主要操作を見失わない。
- 実際のブラウザと支援技術を使って一連の操作を確認している。
モーダルのアクセシビリティは、一つのARIA属性ではなく、意味のある構造、フォーカスの移動、背景の操作制限、退出手段を組み合わせて成立します。
開閉の流れを通してテストすれば、マウス、キーボード、支援技術のどれを使う利用者にも、現在地と次の操作が伝わるダイアログになります。

