ユーザーの現在の作業を「強制的に中断」させる重いUIであり、本当に必要なクリティカルなシーンにのみ限定して使用するコンポーネント。
この記事を読むと、モーダルの適用判断・フォーカストラップ等のA11y要件・代替UIへの逃げ方が自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
現在の画面フローを視覚的・操作的に中断し、重ねて表示されるウィンドウ。背面の操作をロックし、前面のダイアログへの対応を強制する。
PopoverやTooltipとの違い:Modalはフローを「完全に塞ぐ」のに対し、Popover類は背面の操作を許容し、すぐ非表示にできる軽さがある。
3. 使い分け(When to use / When NOT to use)
3.1 When to use
- 取り返しのつかない破壊的アクション(データの完全削除等)の最終確認
- 親画面の文脈を参照しながら、短時間で完了する決定・入力(クイック編集など)
- 規約改定など、システム側からユーザーに「どうしても必ず読んで同意してほしい」情報の通知
3.2 When NOT to use
- 単純な操作完了・成功の通知(Toastを使うべき)
- 非常な長文を入力させる複雑なフォーム(別ページに遷移させるべき)
- モーダルの中でさらにモーダルを開く(多重モーダルは文脈が完全に崩壊する)
3.3 代替UI(Alternatives)
- 成功・エラーの通知 →
Toast/Alert - 長い入力フォーム →
新規ページ遷移
4. 設計判断の核(Decision Principles)
ユーザーのフローを断ち切るに値する「正当な理由」があるか自問する。
判断の優先順位:① 中断の必要性(逃げ道はないか?) → ② 操作の完結性(ここで終わるか?) → ③ フォーカスとA11y担保
- できるだけインライン編集や別ページへの遷移で解決できないか検討し、最後の手段として採用する
- 背景のスクロールは必ずロックし、強制的にモーダル内へ意識を向ける
5. 状態設計(States)
5.1 必須状態(Required)
- Default(通常):中央に配置され、背面に暗いオーバーレイ(Scrim/Backdrop)を敷き前面を強調している状態
- Focus Trap:モーダルが開いている間、Tabキー操作がモーダル内だけでループしている状態
5.2 条件付き状態(Conditional)
- Scroll:コンテンツ超過時、モーダル内部(ヘッダーとフッターを固定し、中間領域のみ)がスクロールしている状態
- Loading:非同期処理中、内部ボタンがスピナー化し、閉じる操作(Escや背景クリック)を一時的に無効にしている状態
5.3 State Gallery
| 状態 | 必須 | 何を伝えるか |
|---|---|---|
| Default | ✅ | 後ろの画面がロックされ、現在ここだけが操作可能であること |
| Focus Trap | ✅ | キーボードとスクリーンリーダーの焦点を強制的に合わせる |
| Scroll | — | 長いコンテンツを確実に見せつつ、アクションボタンを見失わせない |
| Loading | — | 処理中であり、途中で閉じると不整合が起きる可能性があること |
6. バリエーション設計(Variants)
モーダルの役割によってアクションエリアの重みを変える。
| バリアント | 目的 |
|---|---|
| Alert Dialog | 破壊的アクションの確認。「Cancel」と「Destructive(Red)」のペアを置く |
| Confirmation Dialog | 普通の設定保存など。「Cancel」と「Primary(Blue等)」のペアを置く |
| Informational Modal | 規約等の通知。「閉じる(OK)」のみを置く |
禁止パターン:モーダル内から別のモーダルを開く。Z-indexの崩壊や、何のためのモーダルなのかのコンテキスト喪失を招く。
7. パターン集(Good / Bad / How to fix)
7.0 よく崩れる設計パターン(3つ)
- 乱用による作業阻害:多用されすぎるとユーザーは文脈を見失い、惰性ですぐ「閉じる」ようになる
- フォーカスの脱落(A11y違反):開いているのに、背面のページへTabでフォーカスが移動してしまい操作不能に陥る
- スクロールの二重化:背面ページもスクロールできてしまい(スクロール・ジャンク)、画面が不自然に動く
7.1 Bad(典型3つ)
- 「保存しました」という成功通知をわざわざモーダルで出し、OKボタンのクリックを強要する
- モーダルの高さを突き破ってコンテンツが伸び、画面外にはみ出してアクションボタンが押せない
- タブ移動すると、背面のリンクにフォーカスが当たってしまい、見えないまま裏で操作進んでしまう
7.2 Good(対になる3つ)
- 保存通知は数秒で消える Toast または Inline Alert へ変更しフローを止めない
- モーダル自体の最大高(max-height)を画面の80~90%に抑え、内部エリアだけを
overflow-y-autoでスクロールさせる - フォーカストラップを実装し、必ずモーダル内だけでTab移動が完結するようにする
7.3 How to fix(手順)
- 表示したい情報が、ユーザーの作業を「止めてでも」伝えるべきか確認する(不要ならToastへ)
- モーダル背景(Backdrop)が開いた瞬間、
body要素にoverflow: hiddenを当てスクロールを止める - 開くと同時に、モーダル内の最初の入力要素、またはモーダル全体に自動フォーカスを移動する
- Tabキー移動にFocusTrap処理を追加し、外へ逃がさないようにする
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Keyboard
Escキーでモーダルを閉じることができる- フォーカストラップ:
Tabキー移動がモーダル内のみでループし、裏側のページには移動しない
Focus
- モーダルが開いた瞬間、内部の最初のインタラクティブ要素(またはモーダル全体)に自動でフォーカスを移す
- モーダルが閉じた後は、モーダルを開くトリガーとなった元のボタンへフォーカスを確実に戻す
Screen Reader
- コンテナに
role="dialog"またはrole="alertdialog"(重要な確認の場合)を付与する aria-labelledby(タイトル用)とaria-describedby(説明文用)を使用し、開いた瞬間に内容を読み上げさせる
Touch / Pointer
- (任意・推奨)オーバーレイをタップした際にもモーダルを閉じる挙動(Dismiss)をサポートする(入力中フォームの破棄リスクがない場合)
Contrast / Readability
- 背景黒塗りのオーバーレイによって、モーダル本体と元の背面に十分な明度確保と境界線(BorderやShadow)を設ける
10. 実装メモ(Implementation Notes)
- React等では
createPortalなどを利用してDOMのルートへ直接挿入することで、親要素のoverflow: hiddenやz-indexコンテキストによる表示崩れを回避できる - スクロールバーが消えることで画面幅が少し広がりガタつく(layout shift)問題があるため、Bodyのpadding-right等で調整を入れると美しい
- アクセシビリティの考慮事項(ARIA・FocusTrap・Esc対応)が非常に多岐にわたるため、ゼロから作らず標準化されたヘッドレスUIライブラリの利用を強く推奨する
11. 関連リンク
- 関連するUIデザイン原則: フィードバック (Feedback), 明瞭性と確実性 (Clarity & Certainty), エラーの予防 (Error Prevention), 認知特性への配慮 (Cognitive Accessibility)
- 用語集(定義): フォーカストラップ (Focus Trap), アクセシビリティ (Accessibility), コールトゥアクション (Call to Action)
- 関連するUIコンポーネント(横): Toast(トースト通知), AlertDialog(警告ダイアログ), Sheet / Drawer(ドロワー / シート), Button(ボタン)
12. まとめ
ダイアログ(モーダル)は「とりあえず情報を割り込ませる」ための便利なゴミ箱になりがちですが、本質はフローの中断による強制確認です。強力なUIである反面、アクセシビリティの落とし穴(フォーカストラップ等)が非常に多いため、実装時には細心の注意を払い、「本当にダイアログである必要があるか」という問いを常に持ちつづけてください。