重ねる
何かを上に重ねるとき、決めることは1つです。背後を遮るか、遮らないか。残りはそこから決まります。
図の中身を文字で読む
- モーダル 背後を遮断する ドロワー 端から出る ポップオーバー 遮断しない ツールチップ 説明だけ
- 削除しますか 絞り込み 並び替え
- 削除する
- 確認・必須の入力・中断できない処理 モバイルのナビ・フィルター 背後を見ながら操作できる タッチでは出ない
- フォーカスを中へ移し、Escapeで閉じる 出た方向へ戻ることが分かる 中に操作要素を置いてよい 重要な情報を入れてはいけない
- どれを使うかは「背後を遮断するか」で決まる
- 遮断する=完了まで先へ進ませたくないとき。ただし文脈を失わせ、操作を中断するので頻度は最小限にする。
- 入力項目が多い・内容が長い・URLを共有したいなら、モーダルではなく別ページにする。
ひと目で#
- モーダル
- 背後の操作を遮る。終わるまで先へ進ませない
- ダイアログ
- 対話のための窓。モーダルにも、そうでない形にもできる
- ドロワー
- 端から滑り出す。出てきた方向へ戻ればよいと分かる
- ポップオーバー
- 遮らない。中に操作要素を置ける
- ツールチップ
- 説明だけ。タッチでは出ない
- フォーカストラップ
- モーダルの中でフォーカスを循環させる仕組み
使い分け#
| こういうとき | 使うもの |
|---|---|
| 元に戻せない操作の確認 | モーダル |
| モバイルのナビ、フィルター | ドロワー |
| 背後を見ながら絞り込みたい | ポップオーバー |
| アイコンの名前を補いたい | ツールチップ。アクセシブルネームも付ける |
| 入力項目が多い。内容が長い。URL を共有したい | 別のページにする |
| 元に戻せる操作 | 確認をやめ、スナックバーの「取り消す」に置き換える |
間違えやすいところ#
図の中身を文字で読む
- フォーカスの道筋
- 1 開く前の位置 2 開いている間 3 閉じたあと
- 削除する 削除する
- この記事を削除しますか
- この位置を変数に控える 控えた位置へ戻す
- キャンセル 削除する
- Tab は2つの間を回り、背後へ抜けない
- dialog.showModal() なら 2 の閉じ込めと背後の無効化はブラウザが持つ。自分で書くのは 1 と 3 の2行だけ。
- モーダルを開いてもフォーカスを移さないキーボードの利用者が中身にたどり着けない
- 閉じたあとフォーカスを戻さないページの先頭へ飛ぶ
- 背後のスクロールを止めない意図しない移動が起きる
- Escape で閉じられない閉じる手段が限られる
- モーダルに長いフォームを入れる画面が狭く、スクロールが二重になる
- 読み込み直後に自動でモーダルを開く内容を読む前に遮られる
- ツールチップに大事な情報を入れるタッチ端末で読めない
- hover だけでポップオーバーを開くタッチで操作できない
直す例#
架空の案件を1つ例にとります。どこを見て、何をどう直すかを書きます。
記事の管理画面です。「削除」を押すと自作の <div> モーダルが開きますが、Esc では閉じられず、Tab を押し続けると背後の一覧へ抜けます。閉じたあとのフォーカスはページ先頭へ飛び、キーボードで操作していた人は自分がどこにいたかを失います。
自作をやめ、<dialog> と showModal() に置き換えます。フォーカスの閉じ込め、背後の無効化、Esc での閉じは、ブラウザが引き受けます。自分で書くのは、開く前の位置を覚えておき、閉じたら戻す数行だけです。取り消せる削除なら、そもそも確認をやめてスナックバーの「取り消す」に置き換える手もあります。
確認する#
- Esc で閉じられる。閉じたらフォーカスが開いたボタンへ戻る
- 開いている間、Tab が背後に抜けない。背後もスクロールしない
- ツールチップにしか書いていない情報がない。タッチ端末でもすべて読める
この話が要る場面#
このページが答えになる、現場の困り事です。困り事の側から探すと、同じ場所に着きます。
作業を止めてよいならモーダル、元の画面を見ながら操作させたいならドロワー。迷ったらページ遷移。
このコードで動いている見本です
この場面のほかの困り事と、そのまま貼れる部品は部品を選ぶにまとめてある。逆引きには全188件。
開いて確かめる#
この値は作業台で決められます。和文の予約ページに載せて、幅と明暗を替えて確かめてから CSS を持ち帰る: 影
モーダル・小型ポップ・トーストを、この場で実際に開いて確かめます。挙動を見て使うものを選ぶと、フォーカスの扱いまで含めた完成コードが出ます。
モーダル dialog
Esc でも外側のクリックでも閉じる
小型ポップ details
絞り込み
JS なしで開閉。背後は遮らない
トースト
3.5秒で自動的に消える
モーダルを閉じたときは、開いたボタンへフォーカスを戻します(下のコードは記憶と復帰まで含みます)。
<button type="button" id="open-confirm">記事を削除する</button>
<dialog id="confirm" aria-labelledby="confirm-title">
<div class="dialog-in">
<h2 id="confirm-title">この記事を削除しますか</h2>
<p>削除すると元に戻せません。</p>
<form method="dialog">
<button class="button button--ghost" value="cancel">キャンセル</button>
<button class="button button--danger" value="delete">削除する</button>
</form>
</div>
</dialog>
<style>
/* .button の基底と派生(--ghost・--danger)は「出発点のCSS」にある */
dialog { padding: 0; border: 1px solid var(--color-border); }
dialog::backdrop { background: rgb(0 0 0 / 0.4); }
/* 余白は内側の要素に置く。dialog 自体に余白があると、その部分のクリックでも閉じる */
.dialog-in { padding: var(--space-5); display: grid; gap: var(--space-3); }
</style>
<script>
var dialog = document.getElementById("confirm");
var opener = document.getElementById("open-confirm");
var trigger = null;
opener.addEventListener("click", function () {
trigger = document.activeElement; // 開く前の位置を記憶する
dialog.showModal(); // 焦点の閉じ込めと Esc はブラウザが担う
});
dialog.addEventListener("click", function (event) {
if (event.target === dialog) dialog.close(); // 外側のクリックで閉じる
});
dialog.addEventListener("close", function () {
if (trigger) trigger.focus(); // 開いたボタンへ焦点を戻す
});
</script>
この道具は JavaScript で動きます。無効の場合は、出典の MDN「dialog」に同じ型のコードがあります。