通知・ダイアログ
モーダルダイアログ
解説ありDialog (Modal)
背面を操作不能にして前面に出すダイアログ。フォーカストラップと Esc 閉じが要。
モーダルダイアログとは?
モーダルダイアログは、画面の上に重なって開き、閉じるまで背面の操作をブロックする小さなウィンドウです。 設定パネル、フォーム、画像の拡大表示などに使われます。
モーダルで一番むずかしいのはフォーカスの管理です。 幸い、最近のブラウザのネイティブ <dialog> 要素をshowModal() で開けば、その大変な部分が標準で手に入ります。
なぜアクセシビリティが大事なの?
モーダルが正しくないと、こんな問題が起きます。
- キーボード利用者が Tab を押すと、開いているはずのダイアログを すり抜けて背面の見えない要素にフォーカスが移動してしまう(迷子になる)。
- Esc で閉じられない、閉じたあとにフォーカスがどこかへ飛ぶ。
- スクリーンリーダー利用者に、背面のコンテンツまで読めてしまい 「いまダイアログの中にいる」という状況が伝わらない。
ネイティブ <dialog> + showModal() は、これらすべて (フォーカストラップ・Escで閉じる・背面を inert 化・フォーカスの復帰)を 自動でやってくれます。
ライブデモ(推奨実装)
下の「設定を開く」を押すとダイアログが開きます。キーボードだけで操作して、Tab が外に漏れないこと・Esc で閉じることを確かめてください。
試してみよう:Enter で開く → Tab を連打してもフォーカスはダイアログ内をループ → Esc で閉じる → フォーカスが「設定を開く」ボタンに戻る。
ポイント
開いた瞬間、フォーカスはダイアログ内(最初のボタンや autofocus を付けた要素)へ移ります。 閉じると開いたトリガーへ自動で戻るので、利用者は元の文脈を失いません。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| Esc | ダイアログを閉じる(showModal() なら標準で対応) | 必須 |
| Tab / Shift+Tab | フォーカスがダイアログ内だけを循環する(外に漏れない) | 必須 |
| Enter / Space | フォーカス中のボタンを実行 | 必須 |
補足
上のキー操作は <dialog>.showModal() を使えばすべて自動です。 自前の div モーダルだと、これらを一つひとつ JavaScript で実装しなければなりません。
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| ダイアログ本体 | <dialog> 要素 | 暗黙の role="dialog" を持つ。showModal() でモーダルとして開く。 |
| ダイアログ本体 | aria-labelledby="見出しのid" または aria-label | ダイアログに名前を付ける(中の見出しを指すのが定番)。 |
| ダイアログ本体 | aria-describedby(任意) | 補足の説明文を関連付ける。 |
| 背面のコンテンツ | (自動)inert 相当 | showModal() なら背面が自動で操作不可・読み上げ対象外になる。手動なら inert を付ける。 |
実装:推奨パターン(Good)
良い例 / 推奨
ネイティブ <dialog> を showModal() で開く。フォーカス管理・Esc・背面 inert が標準で付いてくる。
マークアップ:
<button type="button" id="open">設定を開く</button>
<dialog id="dialog">
<h2>表示設定</h2>
<p>テーマやフォントサイズをここで変更できます。</p>
<button type="button" id="close">閉じる</button>
</dialog>開閉のスクリプト(たったこれだけ):
const dialog = document.getElementById('dialog');
const openBtn = document.getElementById('open');
const closeBtn = document.getElementById('close');
// showModal() だけで、フォーカストラップ・Esc で閉じる・
// 背面を操作不可(inert)にする、が標準で手に入る。
openBtn.addEventListener('click', () => dialog.showModal());
closeBtn.addEventListener('click', () => dialog.close());
// 閉じたあとは、開いたトリガーへ自動でフォーカスが戻る。補足
ダイアログに名前を付けるため、<dialog aria-labelledby="title"> のように 中の見出しの id を指しておくと、スクリーンリーダーが 「表示設定, ダイアログ」と読み上げてくれます。
アンチパターン(Bad)
下は <div> を重ねただけの自作モーダルです。マウスでは開閉できますが、開いたまま Tab を押すと フォーカスが背面に抜け、Esc でも閉じません。
試してみよう:開いた状態で Tab を連打 → 背面の「設定を開く」やページ内リンクにフォーカスが移ってしまう。Esc を押しても閉じない。× はキーボードでは押せない。
<!-- ❌ div オーバーレイの自作モーダル -->
<button type="button" id="open">設定を開く</button>
<div id="overlay" class="overlay" style="display:none">
<div class="modal">
<h2>表示設定</h2>
<p>テーマやフォントサイズをここで変更できます。</p>
<!-- span なのでキーボードで閉じられない -->
<span class="x" onclick="hide()">×</span>
</div>
</div>悪い例 / 避ける
この実装の問題点:
- Tab が背面に漏れる — フォーカストラップが無く、見えない要素に移動できてしまう。
- Esc で閉じない — キー処理を自分で書いていない。
- 背面が inert でない — スクリーンリーダーで背面まで読めてしまう。
- 閉じるのが
<span>— フォーカスできず、キーボードでは閉じられない。 - フォーカスの移動/復帰が無い — 開いてもダイアログに入らず、閉じても元へ戻らない。
ポイント
どうしても <div> で作るなら、フォーカストラップ・Esc・背面 inert・ フォーカス復帰・role="dialog" / aria-modal="true" をすべて自前で実装する必要があります。<dialog> なら、その大半が無料です。
実装チェックリスト
- ネイティブ
<dialog>をshowModal()で開いている - ダイアログに
aria-labelledby(またはaria-label)で名前がある - 開くとフォーカスがダイアログ内へ移る
- Tab がダイアログ内だけを循環する(背面に漏れない)
- Esc で閉じる
- 閉じると開いたトリガーへフォーカスが戻る
- 背面が
inert(操作・読み上げ不可)になっている - フォーカスリングを消していない