JavaScript Tips | UI・画面部品:Modalの多重表示を防ぐ

JavaScript JavaScript
スポンサーリンク
スポンサーリンク
  1. Modalの多重表示を防ぐユーティリティは「画面を“ごちゃごちゃさせない”ための安全装置」です
  2. Modalの多重表示が起きる典型パターンをイメージする
    1. パターン1:ボタン連打による同じModalの多重表示
    2. パターン2:非同期処理の結果で複数Modalが重なる
    3. パターン3:別のModalが開いているのにさらに別のModalを開く
  3. ステップ1:「今開いているModal」を管理するという発想を持つ
    1. 管理したい情報
  4. ステップ2:ModalControllerに「状態管理」を追加する
    1. 基本となるModalControllerの拡張
    2. ここでの重要ポイント
  5. ステップ3:多重表示を「完全に禁止する」パターン
    1. open の中で何もしないパターン
    2. 使用例
  6. ステップ4:「今のModalを閉じてから次を開く」パターン
    1. 切り替え用のメソッドを用意する
    2. 使用例
  7. ステップ5:ボタン連打対策として「開いている間はトリガーを無効化する」
    1. トリガーボタンを無効化する例
  8. ステップ6:複数種類のModalを扱うときの設計(スタックを使うかどうか)
  9. ステップ7:「多重表示を防ぐ」ことと「エラー表示をきちんと出す」ことのバランス
    1. よくある失敗例
    2. 対策の考え方
    3. 例:エラーモーダルの内容を更新する
  10. ステップ8:業務で使える「Modal多重表示防止ユーティリティ」まとめ
    1. 使用例
  11. ステップ9:絶対に避けるべき「多重表示の危険なパターン」を明確にしておく
    1. 1. DOMを毎回生成してModalを増殖させる
    2. 2. 「今開いているModal」を一切管理しない
    3. 3. エラーをすべて別Modalで出し続ける
  12. まとめ:Modalの多重表示を防ぐユーティリティは「画面を整然と保ち、ユーザーの集中を守るための実務的な技術」です

Modalの多重表示を防ぐユーティリティは「画面を“ごちゃごちゃさせない”ための安全装置」です

業務システムでModal(モーダルダイアログ)を使っていると、 次のような状況に遭遇することがあります。

  • ユーザーがボタンを連打して、同じModalが何度も開いてしまう
  • 非同期処理の結果で、短時間に複数のエラーモーダルが重なって表示される
  • 別のModalが開いているのに、さらに別のModalを開いてしまい、画面が混乱する

こうした 「Modalの多重表示」 は、

  • ユーザーの視線と操作の対象が分散してしまう
  • どのModalを閉じればよいか分からなくなる
  • バグや誤操作の原因になる

という意味で、業務システムでは避けたい挙動です。

そこで役に立つのが、 「Modalの多重表示を防ぐためのJavaScriptユーティリティ」です。

ここでは、初心者向けに、

  • なぜ多重表示が問題になるのか
  • どのように「今開いているModal」を管理するか
  • 実務で使えるユーティリティと設計

をステップバイステップで解説していきます。

Modalの多重表示が起きる典型パターンをイメージする

まず、「どんなときに多重表示が起きるのか」を具体的にイメージしてみます。

パターン1:ボタン連打による同じModalの多重表示

button.addEventListener("click", () => {
  ModalController.open("my-modal");
});
JavaScript

ユーザーがこのボタンを素早く連打すると、

  • CSSやアニメーションのタイミングによっては、 「開く処理」が何度も走る
  • DOMを毎回生成している場合は、同じModalが複数重なってしまう

という状態になりがちです。

パターン2:非同期処理の結果で複数Modalが重なる

fetch("/api/action")
  .then((res) => {
    if (!res.ok) {
      ModalController.open("error-modal");
    }
  });

fetch("/api/another-action")
  .then((res) => {
    if (!res.ok) {
      ModalController.open("error-modal");
    }
  });
JavaScript

短時間に複数のエラーが発生すると、

  • 同じエラーモーダルが何度も開こうとする
  • 別のエラーモーダルが重なって表示される

という状態になります。

パターン3:別のModalが開いているのにさらに別のModalを開く

  • 設定Modalを開いている状態で、 その中からさらに確認Modalを開く
  • どちらも閉じられるが、ユーザーは「どこから来たのか」を見失いやすい

ポイント:Modalの多重表示は「ユーザーの集中を一点に集める」というModal本来の役割を損ない、操作性と信頼性を下げてしまいます。

ステップ1:「今開いているModal」を管理するという発想を持つ

Modalの多重表示を防ぐための基本的な考え方は、

「今開いているModalを1つだけに制限し、 新しく開く前に状態を確認する」

というものです。

管理したい情報

  • 現在開いているModalのID
    • 例:"my-modal"、"error-modal" など
  • 開いているかどうかのフラグ
    • 例:isAnyModalOpen = true/false

これをユーティリティの中で管理することで、

  • すでにModalが開いているときは、新しいModalを開かない
  • 必要なら「今のModalを閉じてから次を開く」

といった制御が可能になります。

ステップ2:ModalControllerに「状態管理」を追加する

前回の「開く/閉じる」ユーティリティに、 「今開いているModal」を管理する機能を追加していきます。

基本となるModalControllerの拡張

export const ModalController = {
  _currentModalId: null,   // 今開いているModalのID(なければ null)

  isOpen() {
    return this._currentModalId !== null;
  },

  getCurrentModalId() {
    return this._currentModalId;
  },

  open(modalId) {
    // すでに何か開いている場合は、ここで制御する
    if (this.isOpen()) {
      // 多重表示を防ぐため、何もしないパターン
      // または、現在のModalを閉じてから開くパターンを選べます
      console.warn("Modal is already open:", this._currentModalId);
      return;
    }

    const el = document.getElementById(modalId);
    if (!el) return;

    el.classList.add("is-open");
    el.setAttribute("aria-hidden", "false");

    this._currentModalId = modalId;
  },

  close(modalId) {
    const el = document.getElementById(modalId);
    if (!el) return;

    el.classList.remove("is-open");
    el.setAttribute("aria-hidden", "true");

    // 現在のModalIDをクリア
    if (this._currentModalId === modalId) {
      this._currentModalId = null;
    }
  },

  closeCurrent() {
    if (!this.isOpen()) return;
    this.close(this._currentModalId);
  },
};
JavaScript

ここでの重要ポイント

  • _currentModalId という内部変数で「今開いているModal」を記録しています。
  • open の中で isOpen() をチェックし、 すでに開いている場合は「何もしない」か「閉じてから開く」かを選べます。
  • close の中で、閉じたModalが現在のものなら _currentModalId を null に戻します。

これにより、「今開いているModalは常に1つだけ」という状態を保ちやすくなります。

ステップ3:多重表示を「完全に禁止する」パターン

まずは一番シンプルな方針として、

「何かModalが開いているときは、新しいModalを開かない」

というルールを実装してみます。

open の中で何もしないパターン

open(modalId) {
  if (this.isOpen()) {
    console.warn("Modal is already open:", this._currentModalId);
    return;
  }
  // 以下は前述のとおり
}
JavaScript

使用例

button.addEventListener("click", () => {
  ModalController.open("my-modal");
});
JavaScript

この場合、

  • ユーザーがボタンを連打しても、最初の1回だけModalが開く
  • すでにModalが開いている状態で別のボタンを押しても、新しいModalは開かない

という挙動になります。

ポイント:業務システムでは「Modalは常に1つだけ」という設計にすることで、 画面の混乱を防ぎやすくなります。

ステップ4:「今のModalを閉じてから次を開く」パターン

場合によっては、

「今のModalを閉じてから、次のModalを開きたい」

というニーズもあります。

例えば、

  • エラーモーダルを閉じたあとに、確認モーダルを開く
  • 設定Modalから、別の詳細Modalに切り替える

などです。

切り替え用のメソッドを用意する

export const ModalController = {
  // 既存のプロパティ・メソッドに加えて

  switchTo(modalId) {
    if (this.isOpen()) {
      this.closeCurrent();
    }
    this.open(modalId);
  },
};
JavaScript

使用例

// ある処理の結果に応じて、Modalを切り替える
if (hasError) {
  ModalController.switchTo("error-modal");
} else {
  ModalController.switchTo("success-modal");
}
JavaScript

このように switchTo を用意しておくと、

  • 「今のModalを閉じてから次を開く」という流れを 1行で表現できるようになります。

ステップ5:ボタン連打対策として「開いている間はトリガーを無効化する」

Modalの多重表示は、 「開いている間にトリガーボタンが何度も押される」ことで起きることも多いです。

そこで、

「Modalが開いている間は、トリガーボタンを一時的に無効化する」

という対策も有効です。

トリガーボタンを無効化する例

function setupModalTriggers() {
  document.querySelectorAll("[data-modal-open]").forEach((btn) => {
    const modalId = btn.getAttribute("data-modal-open");

    btn.addEventListener("click", () => {
      if (ModalController.isOpen()) {
        return; // すでに何か開いているなら何もしない
      }

      // ボタンを一時的に無効化
      btn.disabled = true;

      ModalController.open(modalId);

      // Modalが閉じられたときにボタンを再度有効化する
      const observer = new MutationObserver(() => {
        if (!ModalController.isOpen()) {
          btn.disabled = false;
          observer.disconnect();
        }
      });

      const backdrop = document.getElementById(modalId);
      if (backdrop) {
        observer.observe(backdrop, { attributes: true, attributeFilter: ["class"] });
      }
    });
  });
}
JavaScript

この例は少し高度ですが、 シンプルに「開いている間はボタンを無効化する」だけでも効果があります。

ポイント:ユーザーの連打による多重表示を防ぐには、「開いている間はトリガーを効かせない」という発想が有効です。

ステップ6:複数種類のModalを扱うときの設計(スタックを使うかどうか)

業務システムによっては、

  • 「親Modalの上に子Modalを重ねる」
  • 「確認Modalをさらに重ねる」

といった 「Modalのネスト」 を許容したい場合もあります。

このときの選択肢は2つです。

  1. スタック方式で管理する
    • 開いているModalを配列(スタック)で管理し、 一番上のModalだけを操作する。
  2. ネストを禁止し、常に1つだけに制限する
    • 前述のように _currentModalId だけを管理する。

初心者向け・業務システム向けには、 「ネストを禁止して常に1つだけ」 の方がシンプルで安全なことが多いです。

ここでは、ネストを禁止する前提で話を続けます。

ステップ7:「多重表示を防ぐ」ことと「エラー表示をきちんと出す」ことのバランス

Modalの多重表示を防ぐときに注意したいのは、

「エラーや重要なメッセージが、ユーザーに届かなくなっていないか?」

という点です。

よくある失敗例

  • すでにエラーモーダルが開いている状態で、 別のエラーが発生しても、新しいModalを開かないようにした結果、 後から発生したエラーがユーザーに伝わらない。

対策の考え方

  • 「エラーが連続して発生した場合は、 1つのエラーモーダルの中にメッセージを追記する」
  • 「新しいエラーが来たら、既存のエラーモーダルの内容を更新する」

例:エラーモーダルの内容を更新する

function showErrorModal(message) {
  const modalId = "error-modal";
  const backdrop = document.getElementById(modalId);
  if (!backdrop) return;

  const body = backdrop.querySelector(".modal-body");
  if (body) {
    body.textContent = message;
  }

  if (!ModalController.isOpen()) {
    ModalController.open(modalId);
  }
}
JavaScript

このようにしておけば、

  • すでにエラーモーダルが開いている場合は、 内容だけ更新して多重表示を防ぐ
  • 開いていない場合は、新しく開く

という挙動になります。

ポイント:多重表示を防ぐときは、「メッセージがユーザーに届くこと」を同時に意識する必要があります。

ステップ8:業務で使える「Modal多重表示防止ユーティリティ」まとめ

ここまでの内容を、ひとつのユーティリティとして整理してみます。

export const ModalController = {
  _currentModalId: null,

  isOpen() {
    return this._currentModalId !== null;
  },

  getCurrentModalId() {
    return this._currentModalId;
  },

  open(modalId) {
    if (this.isOpen()) {
      console.warn("Modal is already open:", this._currentModalId);
      return;
    }

    const el = document.getElementById(modalId);
    if (!el) return;

    el.classList.add("is-open");
    el.setAttribute("aria-hidden", "false");

    this._currentModalId = modalId;
  },

  close(modalId) {
    const el = document.getElementById(modalId);
    if (!el) return;

    el.classList.remove("is-open");
    el.setAttribute("aria-hidden", "true");

    if (this._currentModalId === modalId) {
      this._currentModalId = null;
    }
  },

  closeCurrent() {
    if (!this.isOpen()) return;
    this.close(this._currentModalId);
  },

  switchTo(modalId) {
    if (this.isOpen()) {
      this.closeCurrent();
    }
    this.open(modalId);
  },
};

export function showErrorModal(message) {
  const modalId = "error-modal";
  const backdrop = document.getElementById(modalId);
  if (!backdrop) return;

  const body = backdrop.querySelector(".modal-body");
  if (body) {
    body.textContent = message;
  }

  if (!ModalController.isOpen()) {
    ModalController.open(modalId);
  }
}
JavaScript

使用例

// 通常のModal
document.querySelectorAll("[data-modal-open]").forEach((btn) => {
  const modalId = btn.getAttribute("data-modal-open");
  btn.addEventListener("click", () => {
    ModalController.open(modalId);
  });
});

// エラー表示
fetch("/api/action")
  .then((res) => {
    if (!res.ok) {
      showErrorModal("処理に失敗しました。時間をおいて再度お試しください。");
    }
  });
JavaScript

ステップ9:絶対に避けるべき「多重表示の危険なパターン」を明確にしておく

1. DOMを毎回生成してModalを増殖させる

button.addEventListener("click", () => {
  document.body.innerHTML += `
    <div class="modal-backdrop is-open">...</div>
  `;
});
JavaScript

これは、

  • 同じModalが何度もDOMに追加される
  • イベントハンドラが増殖する
  • メモリリークや予期しない挙動の原因になる

ため、業務システムでは避けるべきです。

2. 「今開いているModal」を一切管理しない

  • どのModalが開いているか分からない
  • 閉じる処理が特定のModalだけを対象にしている

これは、バグの温床になります。

3. エラーをすべて別Modalで出し続ける

  • エラーが連続すると、画面がエラーモーダルだらけになる
  • ユーザーがどれを閉じればよいか分からなくなる

結論:Modalの多重表示を防ぐことは、「画面の秩序」を守るための重要な設計です。

まとめ:Modalの多重表示を防ぐユーティリティは「画面を整然と保ち、ユーザーの集中を守るための実務的な技術」です

Modalの多重表示を防ぐためのJavaScriptユーティリティと設計を導入することで、次のような効果があります。

  • 常に「今開いているModalは1つだけ」という状態を保てる
  • ボタン連打や連続エラーによる画面の混乱を防げる
  • エラーメッセージや確認ダイアログを、ユーザーに分かりやすく提示できる
  • チーム全体で「Modalの安全で整然とした使い方」を共有しやすくなる

業務システムにおいて、Modalは「重要な情報や判断をユーザーに提示する場」です。

その場が多重表示でごちゃごちゃにならないようにすることは、 ユーザー体験とシステムの信頼性を守るうえで、とても実務的で重要な技術だと言えます。

タイトルとURLをコピーしました