modal-and-overlay-patterns について
オーバーレイの選択を支援する、デザイナーやフロントエンド開発者向けの UI 設計スキルです。軽量なポップオーバーとブロッキングダイアログを区別し、共通の操作性とアクセシビリティの指針を示します。
主な機能
- 01ブロック度に応じた選択
- 02ツールチップとポップオーバーの規則
- 03選択メニューとアクションメニューの区別
- 04モバイル用ボトムシートの規則
- 05モーダルの終了と重ね合わせ
- 06メディアビューアーのズーム操作
活用シーン
- 01破壊的操作の確認フロー設計
- 02モバイルナビゲーションパネルの選定
- 03製品内のオーバーレイ様式の統一
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dembrandt/ dembrandt-skills --skill modal-and-overlay-patternsインストール後、Claude Code で/modal-and-overlay-patternsを入力して読み込まれたか確認します。
詳細
モーダルとオーバーレイのパターン
オーバーレイはメインコンテンツレイヤーの上に表示されます。軽量なポップオーバー(操作を妨げず、トリガーに紐づくもの)から、完全に操作をブロックするモーダル(アプリが続行する前にユーザーの応答を必要とするもの)まで、さまざまな種類があります。タスクに適したオーバーレイの種類を選ぶことで、不要な割り込みを防ぎ、ユーザーが現在の状況を把握しやすくなります。
オーバーレイの階層
タスクを満たす最も軽量な種類を選んでください。より重いオーバーレイほど、認知的負荷が高くなります。
| 種類 | 背景をブロックする | トリガーに紐づく | 一般的なコンテンツ | 閉じ方 |
|---|---|---|---|---|
| ツールチップ | いいえ | はい | 説明文1~2行 | カーソルを外す/フォーカスを外す |
| ポップオーバー | いいえ | はい | フォームフィールド、ピッカー、小さなリストなどの短い対話型コンテンツ | 外側をクリック、Escape、明示的に閉じる |
| ドロップダウン/メニュー | いいえ | はい | アクションまたは選択肢のリスト | 外側をクリック、Escape、選択 |
| ボトムシート(モバイル) | 部分的(暗転) | いいえ | 小さな画面でのアクションまたはコンテンツ | 下にスワイプ、スクラムをタップ、Escape |
| ドロワー/サイドパネル | 部分的(暗転) | いいえ | 副次的な編集、詳細ビュー、長いフォーム | Escape、明示的に閉じる。スクラムのクリックも可能 |
| ダイアログ/モーダル | はい(スクラムで全面) | いいえ | 操作をブロックするタスク:アクションの確認、必須フォームへの入力 | Escape(破壊的でない場合のみ)、明示的なボタン |
| 全画面オーバーレイ | はい(完全にブロック) | いいえ | 没入型タスク:メディアビューアー、複雑な設定 | 明示的に閉じる場合のみ |
判断基準: オーバーレイを開いたままアプリのほかの部分を使い続けられる場合は、ブロックしない種類(ドロワー、ポップオーバー)を使用してください。アプリがユーザーの応答を待つ必要がある場合は、モーダルを使用してください。
あらゆる種類に共通する1つのシェル
ツールチップ、ポップオーバー、ドロワー、ボトムシート、ダイアログは、5つの別々のコンポーネントではなく、1つのファミリーです。共通のシェルを使います。同じ隆起したサーフェス、1つの細い境界線、1つの角丸、そしてモーダル性に応じて強くなるシャドウです。ヘッダーとフッターを備えるものでは、内部の構成も共通です。対象を示すタイトル、ヘッダー下の細い線、そしてどこでも統一されたフッターの配置順です。種類によって変わるのはサイズ、配置(矢印付きまたは中央揃え)、ブロックの有無だけです。
オーバーレイごとに個別にスタイルを設定すると、ユーザーは同じメッセージの異なる方言に出会うことになります。角丸、ボタンの順序、確認ラベルがそれぞれ異なります。そのたびに再学習のための時間がかかり、破壊的な操作の確認を読み違えやすくなります。シェルを1つにすれば、コードのプリミティブとデザインツールのファミリーも1つになり、修正をすべての箇所に反映できます。
追加する前に監査してください。 プロダクト内のすべてのフローティングサーフェスと、それぞれが使用するトークンを一覧にしてください。シェルを1つのプリミティブとして切り出し、その上に各オーバーレイの種類を構築します。よくある問題は、確認用ポップオーバーとダイアログでフッターの順序が異なることや、ほかのUIでは使われなくなった重い罫線がダイアログのヘッダーに残っていることです。
ツールチップ
ツールチップは、ホバーまたはキーボードフォーカス時に表示され、トリガーのフォーカスが外れると消えます。純粋に情報を提示するもので、内部に対話型の要素は含めません。
- コンテンツ:短い文、ラベル、またはキーボードショートカットを1つ。ツールチップ内にリンクやボタンを置かないでください。
- 表示遅延:ホバー時は300~400ms、キーボードフォーカス時は遅延なし。
- 位置:トリガーの上を優先し、ビューポートの端が近い場合は自動的に反転させます。
- ARIA:要素に
role="tooltip"を指定し、トリガーのaria-describedbyでツールチップのIDを参照します。
ポップオーバー
ポップオーバーはトリガーに紐づきますが、カラー選択ツール、日付範囲セレクター、小さなフォーム、フィルターのリストなど、対話型のコンテンツを含みます。ツールチップとは異なり、ユーザーが操作している間は開いたままになります。
- 最大幅:280~360px。コンテンツがこれより大きい場合は、ドロワーを使用してください。
- 位置:トリガーに紐づけ、ビューポート内に収まるよう自動的に反転させます。
- 閉じ方:外側をクリック、Escapeキー、またはコンテンツが長い場合は明示的な閉じるボタン。
- フォーカス:開いたときにポップオーバー内へフォーカスを移動し、閉じたときにトリガーへフォーカスを戻します。
- ARIA:対話型の場合は
role="dialog"、リストの場合はrole="listbox"を指定し、トリガーにaria-haspopupを設定します。
ドロップダウンとメニュー
ドロップダウンは、トリガーボタンに紐づいた選択肢や操作の一覧を表示します。インタラクティブなオーバーレイの中で最も軽量なものです。
- 選択用ドロップダウン(ユーザーが値を1つ選び、その値が保持される)と、アクションメニュー(値として保持されない操作をユーザーが実行する)を区別してください。
- 幅:トリガーと同じ幅以上にし、最大280pxにします。
- 項目数が多い場合:項目が8~10個を超える場合は、上部に検索入力欄を追加します。
- キーボード操作:
↑/↓で項目間を移動し、Enterで選択し、Escapeで閉じます。
ボトムシート(モバイル)
小さな画面では、モーダルやポップオーバーの代わりにボトムシートを使います。画面の下端からスライドして現れ、タッチデバイスに自然な操作感をもたらします。
- のぞき見できる高さ: 最初はシートの一部(ハンドルとタイトル)だけを表示し、ユーザーがドラッグして展開できるようにします。
- 全画面表示: 内容が長い場合や、ビューポート全体を必要とするフォームに使います。
- 閉じる方法:下にスワイプする、スクリーンをタップする、またはEscapeキーを押します。
- モバイルではダイアログを画面中央に配置せず、代わりにボトムシートを使います(中央配置のモーダルは小さくなりすぎ、操作しにくくなります)。
- ARIA:
role="dialog"として扱い、モーダルと同じフォーカス管理を適用します。
ドロワー/サイドパネル
ドロワーは左右どちらかからスライドして現れ、メインコンテンツを部分的に覆います。メインコンテンツを使いながら参照する可能性がある、補助的な編集作業、詳細表示、設定に使います。
- 右ドロワー: 詳細表示、編集フォーム、フィルター/並べ替えパネル。最も一般的です。
- 左ドロワー: モバイルでのナビゲーション(ハンバーガーメニューのパターン)。
- 幅:デスクトップでは320~480px。モバイルでは全幅にします(実質的にボトムシートまたは全画面オーバーレイになります)。
- スクリーン:ドロワーの背後に半透明の背景(
rgba(0,0,0,0.4))を表示し、メインコンテンツを暗くします。 - 閉じる方法:Escapeキー、明示的な閉じるボタン。スクリーンのクリックによる閉じ方は任意です。未保存のフォームがある場合は避けてください。
- タスクの進行を妨げる場合(必須の判断など)にドロワーを使わないでください。代わりにモーダルを使います。
- ARIA:
role="dialog"、aria-modal="true"を設定し、aria-labelledbyでドロワーのタイトルを指定します。
モーダル/ダイアログ
モーダルは画面全体をスクリーンで覆い、UI全体を操作できない状態にします。ユーザーの応答がなければアプリを続行できない場合に限って使います。
モーダルを使う場合
- 破壊的または取り消せない操作を確認する場合
- 続行前に送信が必要な必須フォーム
- ユーザーの確認が必要なエラーや警告
モーダルを使わない場合
- ユーザーが都合のよいときに読める情報を表示する場合 → インライン通知または通知を使います
- メインコンテンツと並行して行いたいタスクの場合 → ドロワーを使います
- 項目が多い大きなフォームの場合 → 専用ページまたはドロワーを使います
タブを閉じる場合
beforeunloadではブラウザー独自の汎用ダイアログが表示されるだけです。それ以外の処理は実行されず、ダイアログの表示からアンロードまでの間にカスタムマークアップが描画されることもありません。この動作を利用した「離れる前に評価する」フローや「変更内容を確認する」フローを設計しないでください。アプリ側で制御できる操作(閉じるボタン、ルート変更、新しい検索)を対象にし、タブを閉じる操作は回避手段として扱います。
構成
┌──────────────────────────────────┐
│ Title [✕] │ ← Header: title + close button
├──────────────────────────────────┤
│ │
│ Body content │ ← Content: scrolls if needed
│ (description, form, media) │
│ │
├──────────────────────────────────┤
│ [Cancel] [Confirm]│ ← Footer: actions, right-aligned
└──────────────────────────────────┘
サイズ
| サイズ | 幅 | 用途 |
|---|---|---|
| 小 | 360px | 短い確認、単一フィールドのプロンプト |
| 中 | 480px | 標準的なダイアログ、短いフォーム |
| 大 | 640px | 複数フィールドのフォーム、情報量の多いコンテンツ |
| 全画面 | 100% viewport | 没入感のあるタスク。使用は控えめに |
モーダルの高さを超えるコンテンツは、本文領域内のみでスクロールするようにします。ヘッダーとフッターは常に表示されたままにします。
モバイルファーストのダイアログには、独自のデスクトップレイアウトを用意します。 大画面の中央に配置されたスマートフォン幅のカードは、窮屈で未完成に見えます。選択肢は狭い1列に折り返され、項目同士の間隔も十分ではありません。モバイルでは元のレイアウトを維持しながら、デスクトップのブレークポイントには専用の幅、列数、間隔を設定します。
選択肢ごとにサブ選択肢が2つか3つの場合は、インラインで表示します。 ピッカーの各選択肢に第2階層の選択肢が2つほどしかない場合は、常に表示されるリンクとして選択肢の下に配置します。アコーディオンや2つ目のダイアログではクリックが必要になり、利用可能な選択肢が見えにくくなります。また、第2の条件を別のセレクターで選ぶ形式では、存在しない組み合わせを選べてしまいます。
閉じる動作
| トリガー | 破壊的でない操作で許可 | 破壊的な操作で許可 |
|---|---|---|
Escape キー | はい | いいえ — 明示的なキャンセルが必要 |
| スクリムの外側をクリック | はい(任意) | いいえ — 誤って閉じやすいため |
| 閉じるボタン(✕) | はい | はい |
| キャンセルボタン | はい | はい |
破壊的な操作や取り消せない操作では、Escape キーと外側のクリックによる閉じる動作を無効にします。ユーザーはキャンセルまたは確認を明示的に押す必要があります。
モーダルの重ね合わせ
モーダルの中から別のモーダルを開くのは避けてください。これは情報設計に問題がある兆候です。タスクが複雑すぎて、1つのダイアログでは扱いきれない可能性があります。
2つ目のオーバーレイが避けられない場合は、別の全画面モーダルではなく、モーダル内にアンカーされたポップオーバーを使います。ブロックするスクリムオーバーレイを2つ重ねることは避けてください。
ライトボックス/メディアビューアー
ライトボックスには、目に見えるタイトルや本文がない場合がありますが、閉じる、ページ移動、ズームなどの操作コントロールがあるため、アクセシブルな名前を持つダイアログでなければなりません。スクリーン、閉じる操作、フォーカス、テキストによる代替に関するルールは引き続き適用されます。ズームとページ移動が操作のすべてであり、フォームと同じくらい丁寧に設計する必要があります。
クリックした項目を開きます。 各サムネイルをクリックすると、最初の画像ではなく、そのサムネイル自身のインデックスでビューアーが開きます。Escape キーで閉じます。開いたときは閉じるボタンにフォーカスを移し、閉じたときはサムネイルにフォーカスを戻します。
画面幅ではなく、ポインターの種類に応じて入力モデルを選びます。 実行時に matchMedia('(pointer: fine)') を使ってデフォルトを選びます。タッチスクリーンを備えたノートパソコンも、トラックパッドを備えたタブレットもあるためです。これはプライマリポインターしか判定しないため、両方のモデルを利用可能にし、それぞれのイベントの pointerType で判断してください。
- ファインポインター:クリックした位置を中心にズームを切り替えます。パンには、ポインターを動かす方法(すばやく操作できますが、閉じるボタンに手を伸ばすたびに画像が動きます)と、ドラッグする方法(より落ち着いて操作できますが、覚えるジェスチャーがもうひとつ増えます)があります。どちらかを選び、製品全体で統一してください。
- コースポインター:ピンチでズームし、ダブルタップで切り替え、ドラッグでパンします。これらはユーザーがプラットフォームで慣れ親しんでいる操作です。
ジェスチャーだけでは不十分です。 目に見える +/− ボタンを用意すると、ズームを見つけやすく、キーボードからも操作できるようになります。1×では − を、最大倍率では + を無効にしてください。そうすることで、ボタンが現在の状態も伝えます。+/− キーや矢印キーでパンできるようにするのも、同じルールに沿った適切な拡張です。
2 段階のズーム。 クリックまたはダブルタップすると、ユーザーが触れた位置を中心に、固定の詳細倍率に切り替わります。ボタンを使うと、さらに高い最大倍率まで段階的に拡大できます。倍率は素材に応じて設定します。たとえば、商品写真なら 2× と 4×、地図、スキャン画像、細かい図表ならさらに高い倍率が必要です。一度に最大倍率まで拡大すると、ユーザーはどこを見ているのかわからず、ぼやけたピクセルだけが表示されてしまいます。
カーソルで次の操作を示します。 1×ではズームインカーソル、拡大中はズームアウトカーソル、ビューアーを開くサムネイルには拡大カーソルを表示します。ズームの実装が拡大中の画像に独自の cursor: move を設定する場合は上書きしてください。そうしないと、操作中にヒントが変わってしまいます。
ドラッグとクリックを区別します。 押してから離すまでに数ピクセル(約 5 px)以上動いた場合はパンとみなし、離したときにズームを切り替えてはいけません。そうしないと、パンするたびに画像が 1×に戻ってしまいます。
ズームは画像ごとに適用します。 次の画像に移動するときは 1×にリセットし、画像が 1×に戻ったらスワイプで再びカルーセルを操作できるようにします。
オーバーレイ内のツールの位置を固定します。 閉じるボタンは右上、ページャーは下部中央、ズームコントロールは右下に配置します。ユーザーは一度覚えた配置を頼りにするため、画面ごとに動かすと学習が無駄になります。
状態を伝えます。 すべてのアイコンボタンに aria-label を付けます。それに加えて、アクセシビリティの基本として、ほかのダイアログと同様に現在の倍率をスクリーンリーダーに伝えます。ボタンのラベル(「拡大、現在 2×」など)に含めるか、丁寧な通知を行うライブリージョンを使ってください。また、ズームのトランジションでは prefers-reduced-motion を尊重します。[[wcag-accessibility]] を参照してください。
フォーカス管理
すべてのオーバーレイで、フォーカスを適切に管理する必要があります。フォーカス管理の不備は、アクセシビリティに関する最もよくある問題のひとつです。
開いたとき
- オーバーレイ内の最初のインタラクティブ要素(通常は最初の入力欄、確認ダイアログでは確定ボタン)にフォーカスを移します。
- フォーカスをトラップします。
TabとShift+Tabでオーバーレイ内だけを循環し、スクリーンの背後にあるコンテンツへフォーカスが移動しないようにします。
閉じたとき
オーバーレイを開くきっかけとなった要素にフォーカスを戻します。トリガーが存在しなくなっている場合(その要素が削除された場合)は、近くの論理的な要素にフォーカスを移します。
実装上の注意
フォーカストラップライブラリを使うか、最新のブラウザーでネイティブにフォーカストラップを処理する <dialog> 要素を使ってください。手動でフォーカストラップを実装すると、エラーが起きやすくなります。
モーダルとドロワーの ARIA
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Delete project?</h2>
<p id="modal-description">
This will permanently delete "Apollo" and all its data. This cannot be undone.
</p>
<button>Cancel</button>
<button>Delete</button>
</div>
- コンテナーに
role="dialog"を設定します aria-modal="true"は、スクリーンリーダーにオーバーレイの背後にあるコンテンツを無視するよう伝えますaria-labelledbyには、ダイアログのタイトルの id を指定しますaria-describedbyには、説明の id を指定します(任意ですが、確認ダイアログでは役立ちます)- スクリムの背景には
aria-hidden="true"を設定します。スクリーンリーダーで読み上げられてはいけません
破壊的操作の確認パターン
破壊的な操作の確認ダイアログでは、対象の項目とその結果を明示する必要があります。「本当によろしいですか?」のような一般的なダイアログでは、十分な情報が伝わりません。
推奨:
「Apollo Project」を削除しますか? すべてのタスク、ファイル、履歴が完全に削除されます。この操作は元に戻せません。 [キャンセル] [プロジェクトを削除]
非推奨:
この操作を実行してもよろしいですか? [いいえ] [はい]
- 主要な破壊的操作ボタンには
--color-primaryではなく、--color-error/--color-dangerを使用します。 - 破壊的操作ボタンには、「プロジェクトを削除」「メンバーを削除」「注文をキャンセル」のように、内容が明確なラベルを付けます。「OK」や「確認」だけにしてはいけません。
- キャンセルは常に左側(または副次的な位置)、破壊的操作は右側に配置します。
密着セル上のツールチップ
密着したグループ内のセルに表示するツールチップは、グループ全体を避けて開きます。グリッド、ツリーマップ、セグメントマークなどでセルが密着している場合、各セルのツールチップはグループ全体の上または横に配置し、隣接するセルに重ねてはいけません。下段のセルに固定されたツールチップが、その読み手が次に指そうとしている行を隠してしまうためです。ツールチップがポインターを無視するようにして、隣のセルへ移動したときに次のツールチップが開くようにします。
レビューチェックリスト
- タスクに適したオーバーレイの種類(ツールチップ/ポップオーバー/メニュー/ボトムシート/ドロワー/モーダル)が選ばれていますか?
- ドロワーやインラインパターンで十分な場合に、モーダルを避けていますか?
- モーダル/ドロワーに、視認できるタイトル、本文、明確なアクションボタンがありますか?
- オーバーフローしたコンテンツは本文内でスクロールし、ヘッダーとフッターは固定されていますか?
- 破壊的操作では Escape が無効になっていますか(外側をクリックした場合も同様ですか)?
- 開いたときにフォーカスがオーバーレイ内へ移動し、閉じたときにトリガーへ戻りますか?
- 開いている間、フォーカスはオーバーレイ内に閉じ込められていますか?
-
role="dialog"、aria-modal="true"、aria-labelledbyは正しく設定されていますか? - 破壊的操作の確認ダイアログで、対象の項目とその結果が明示されていますか?
- 破壊的操作ボタンには、「OK」や「確認」ではなく、明確なラベルが付いていますか?
- モーダルの多重表示を避けていますか?
-
beforeunloadを中心にしたカスタム UI を設計していませんか? - モバイルでは、モーダルをボトムシートに置き換えていますか?
- ライトボックス:クリック/パンの操作モデルはポインターの種類に応じていますか。また、ジェスチャーの横に
+/−ボタンとキーが配置されていますか? - ライトボックス:カーソルにはズームイン/ズームアウトが表示され、パン操作の終了時にズームが切り替わることはありませんか?
- ライトボックス:ページ切り替え時にズームがリセットされ、現在の倍率がスクリーンリーダーに公開されていますか?
- 密着セル上のツールチップはグループの外側に開き、ポインターが隣のセルへ移動できるようになっていますか?
- すべてのオーバーレイで同じシェル(サーフェス、境界線、角丸、影のスケール)とフッターの順序が統一されていますか?