モーダル管理を“アプリ全体で破綻しないレベル”へ仕上げる 6日目のテーマ
6日目は、1〜5日目で作ってきた Modal クラス と ModalManager をさらに強化し、
「アプリのどこに置いても壊れないモーダル」を完成形に近づける回です。
今日の学習ポイントは次の3つです。
- クラス設計を“責務の分離”という観点で整理し直す
- UI 状態制御を「外部環境(スクロール・フォーカス)」まで含めて扱う
- イベント伝播を応用し、複雑な UI でも誤作動しない仕組みを作る
実装としては、
開く / 閉じる、背景クリック、ESCキー対応を
アプリ全体の UX と整合する形 に仕上げます。
モーダルが開いている間の「外部スクロール」を制御する
モーダルが開いているのに背景がスクロールする問題
初心者が最初にぶつかるのがこれです。
- モーダルを開く
- 背景がスクロールしてしまう
- モーダルがズレて UX が崩れる
これは、モーダルを開いたときに
body のスクロールを止めていない ことが原因です。
スクロールを止める実装
lockScroll() {
document.body.style.overflow = "hidden";
}
unlockScroll() {
document.body.style.overflow = "";
}
JavaScriptopen() と close() に組み込む
open() {
if (this.state !== "closed") return;
this.state = "opening";
this.root.classList.add("is-open");
this.lockScroll();
}
close() {
if (this.state !== "opened") return;
this.state = "closing";
this.root.classList.remove("is-open");
this.unlockScroll();
}
JavaScript深掘りポイント:スクロール制御は「モーダルの責務」
モーダルが開いている間は
ユーザーの視線をモーダルに固定する 必要があります。
そのためには、
- 背景スクロール禁止
- 背景クリック制御
- ESC キー対応
これらがセットで必要になります。
フォーカス制御で「キーボード操作に強いモーダル」にする
モーダルが開いたときの問題
- Tab キーで背景のボタンにフォーカスが移動してしまう
- キーボード操作がモーダルの外に飛んでしまう
これはアクセシビリティの観点でも NG です。
解決策:モーダル内にフォーカスを閉じ込める
trapFocus(event) {
const focusable = this.root.querySelectorAll(
'button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
JavaScriptopen() でフォーカスをセット
open() {
this.state = "opening";
this.root.classList.add("is-open");
this.lockScroll();
this.root.addEventListener("keydown", this.trapFocus.bind(this));
this.content.querySelector("button, input")?.focus();
}
JavaScript深掘りポイント:フォーカス制御は“プロ品質のモーダル”の必須要素
実務では、
フォーカスが外に逃げるモーダルは不具合扱い です。
キーボード操作だけで使える UI を作るためにも、
フォーカス制御は欠かせません。
背景クリック制御を「複数レイヤー対応」にする
モーダルの上に別のモーダルが開くケース
例:
設定モーダル → 中の「削除確認」モーダル
このとき、背景クリックの判定が難しくなります。
解決策:ModalManager に「最前面モーダル」を問い合わせる
class ModalManager {
getTopModal() {
return this.stack[this.stack.length - 1];
}
}
JavaScriptモーダル側で判定する
handleRootClick(event) {
const top = this.manager.getTopModal();
if (top !== this) return; // 最前面でなければ無視
const isInside = this.safeAreas.some(el => el.contains(event.target));
if (!isInside) this.close();
}
JavaScript深掘りポイント:複数モーダルがあるときは“最前面だけが操作対象”
ユーザーが見ているのは最前面のモーダルだけです。
背景クリックも ESC も、
最前面のモーダルだけが反応する のが正しい挙動です。
ESC キー対応を「状態遷移」と連動させる
4〜5日目の ESC 対応
- ESC で閉じる
- disableEsc で無効化できる
ここに 6日目では
状態遷移との連動 を追加します。
実装
handleKeydown(event) {
if (event.key !== "Escape") return;
if (this.disableEsc) return;
if (this.state !== "opened") return;
this.close();
}
JavaScript深掘りポイント:アニメーション中の ESC は絶対に無効
アニメーション中に ESC を受け付けると、
- 開き途中で閉じる
- 閉じ途中でさらに閉じる
- 状態がズレる
という UI 崩壊が起きます。
状態遷移と ESC を連動させることで
モーダルの動きが常に一貫する ようになります。
クラス設計を「責務の分離」で整理し直す
Modal の責務
- UI の状態管理
- 開く / 閉じる
- フォーカス制御
- 背景クリック制御
- アニメーション同期
ModalManager の責務
- 複数モーダルのスタック管理
- ESC キーの一元管理
- 最前面モーダルの判定
深掘りポイント:責務を分けると“壊れにくくなる”
UI コンポーネントは、
1つのクラスに責務を詰め込みすぎると壊れやすい
という性質があります。
Modal と ModalManager を分けることで、
- どこを直せばいいか分かりやすい
- 機能追加がしやすい
- バグが起きにくい
というメリットが生まれます。
6日目のまとめ
今日あなたがやったことを整理するとこうなります。
- モーダル開閉時のスクロール制御を導入し、背景の動きを止めた
- フォーカス制御を追加し、キーボード操作に強いモーダルにした
- 背景クリックを「最前面モーダルだけが反応する」形にした
- ESC キー対応を状態遷移と連動させ、アニメーション中の誤作動を防いだ
- Modal と ModalManager の責務を整理し、拡張しやすい設計にした
どれも、実務でモーダルを扱うときに必ず必要になるテクニックです。
7日目では、
「モーダルをアプリ全体の UI コンポーネントとして完成させる」
という最終仕上げに入ります。

