モーダル
ボタンでフェード+スケールインしながらモーダルが中央に開く。背景クリック・×ボタン・Escキーで閉じる。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- はい/いいえの単純な確認
- 必ず読んで判断してほしい内容
- 背景を暗くして視線を集めたい割り込み
- 避ける場面
- 入力項目が 5 個を超える長いフォーム
- 通知だけで判断が要らない内容
- 同時に 2 つ開く
- 構成要素
- 開くボタン
- バックドロップ
- モーダル
- タイトル
- 閉じるボタン
- キャンセル/OK
- 持っている状態
- 通常
- ホバー
- フォーカス
- 押下中
- キーボード
Enter/Space で開くと先頭の閉じるボタンへ、Tab/Shift+Tab は中で循環、Esc・キャンセル・OK・背景クリックで閉じて元のボタンへ戻る。閉じている間は中のボタンに Tab が入らない
- 支援技術
APG Modal Dialog: role=dialog+aria-modal=true+aria-labelledby、閉じている間は inert+visibility:hidden、フォーカスの移動と復帰。閉じるは aria-label
- 動き
出現 220ms(0.88→1 スケール・軽い overshoot)、背景 220ms。reduced-motion で停止
デザインの要点
- 出現は scale 0.88→1 と opacity を 220ms で同時に動かす。位置は動かさず、中央から「浮き上がる」ように見せる
- 閉じている間は inert と visibility:hidden の両方を当てる。opacity:0 だけでは見えないボタンに Tab が入る
- 開いたら中の先頭要素へ、閉じたら開く前にフォーカスがあった要素へ戻す。戻さないと文書の先頭に飛ぶ
- Esc は開いているときだけ拾う。常時 close を呼ぶと、同じページの別のオーバーレイまで巻き添えで閉じる
プレビューは枠内に収めるため、オーバーレイを position:absolute(ステージ基準)で実装しています。実際のページで画面全体に重ねるときは position:fixed に変え、body 直下など高い階層へ置いてください。
使いどころ
「はい/いいえ」のような単純な確認や、削除などの取り消せない操作の最終確認に向く。画面全体を暗くして視線をモーダルへ強制的に集める働きがあるため、ユーザーに必ず読んでほしい・必ず判断してほしい内容だけに使う。逆に、入力項目が5個を超えるような長いフォームや、複数ステップにまたがる作業をモーダルに詰め込むのは避ける。狭い枠の中でスクロールが発生し、下書きの保存もしづらく、閉じ忘れると入力内容が消える。そうした作業は専用ページかドロワーに任せた方がよい。また、通知だけが目的でユーザーの判断を必要としない場合はトーストで足りる。モーダルはユーザーの操作を完全に止める最も強い割り込みなので、1画面で同時に2つ開かないこと、そして開いた理由に対応する選択肢(OK/キャンセルなど)を必ず用意することが前提になる。
仕組み
backdrop と modal を別要素として重ね、開閉は is-open クラスの付け外しだけで行う。backdrop は opacity、modal は transform: scale() と opacity をトランジションさせ、cubic-bezier(0.34,1.36,0.64,1) で終盤にわずかに伸びてから収まる「弾む」イージングを使っている。scale/opacity/transform だけをアニメーションさせているのはレイアウト再計算を避けるため。開くときは FOCUSABLE セレクタで modal 内の操作可能要素を集めて先頭へフォーカスを移し、開く直前の document.activeElement を trigger として保持しておき、閉じたときにそこへフォーカスを戻す。Tab キーは document 側の keydown で監視し、最初と最後の要素で折り返すことでフォーカスをモーダルの外へ逃さない、いわゆるフォーカストラップを自前で実装している。
実装の注意
最大の落とし穴は、閉じている間の .modal の中身がキーボードでフォーカス可能なままになる構成。非表示を opacity と pointer-events だけで行うと、Tab トラップは isOpen が true のときしか働かず、モーダルを開く前や閉じた後にページ本体を Tab で辿ると、見えないはずのキャンセル/OKボタンにフォーカスが飛んでしまう。この見本では閉じるタイミングで inert 属性を付け、CSS 側でも visibility:hidden(透明化の終了後に切り替わるよう transition に visibility 0s linear 0.22s を含める)を当てて二重に防いでいる。inert を外すだけでこの事故が再現する。また、Escape のキー監視は isOpen を見てから closeModal() を呼ぶ。常時閉じる実装にすると、他のオーバーレイと共存させる画面で想定外のモーダルまで閉じるので、「今アクティブなオーバーレイはどれか」を管理する仕組みに直す必要がある。
アクセシビリティ
WAI-ARIA の Dialog (Modal) パターンに沿って role="dialog"・aria-modal="true"・aria-labelledby でタイトルと紐付け、開いたら最初の操作要素へフォーカスを移し、閉じたら開く前にフォーカスがあった要素へ戻す、という一連の型を実装している。閉じている間は aria-hidden="true" に加えて inert と visibility:hidden を当てる。aria-hidden を付けた要素の中に focusable な要素が残っているのは ARIA の仕様違反で、支援技術によっては aria-hidden 自体を無視してしまうため、読み上げからもフォーカス順からも同時に外す必要がある。aria-modal="true" は背景コンテンツを支援技術から隠す指示だが実装差があるため、可能なら背景側にも inert を当てて確実に読み上げ対象から外す。prefers-reduced-motion ではバックドロップとモーダル両方のトランジションを丸ごと止め、フォーカスは :focus-visible で 2px の輪郭を描く。
調整して使う
触りやすいのは3つ。まずイージングで、cubic-bezier(0.34,1.36,0.64,1) の「弾み」を弱めたいときは overshoot の少ない ease-out に変えると落ち着いた印象になる。次にバックドロップの濃さ(既定 rgba(44,40,35,0.45))で、フォームなど注視させたい内容ほど濃く、軽い確認ほど薄くする。最後にモーダルの出現位置とサイズで、デモは中央固定・幅210pxだが、フォームを載せるなら幅を広げて上寄りに配置すると入力中にソフトウェアキーボードで隠れにくい。なお、背景クリックとEscapeでの閉鎖は「取り消し可能な操作」向けの挙動なので、削除の最終確認のように誤操作を避けたい場面ではこの2つを外し、明示的なボタン操作だけで閉じるようにするとよい。
コード(コピーして使えます)
<div class="stage" id="stage">
<button class="open-btn" id="openModal">モーダルを開く</button>
<div class="backdrop" id="backdrop" aria-hidden="true"></div>
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-hidden="true" inert>
<div class="modal-header">
<p class="modal-title" id="modalTitle">確認</p>
<button class="close-btn" id="closeModal" aria-label="閉じる">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<line x1="3" y1="3" x2="13" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="3" x2="3" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<p class="modal-body">この操作を続けますか?<br>変更内容は保存されます。</p>
<div class="modal-footer">
<button class="btn-cancel" id="cancelModal">キャンセル</button>
<button class="btn-ok" id="okModal">OK</button>
</div>
</div>
</div>