U-Code

<dialog>を使用したモーダルウィンドウ

コーディング

アクセシビリティに配慮した<dialog>を使用したモーダルウィンドウのメモです。

HTML / CSSと素のJavaScriptで記述しています。


追記

モーダル内の「前へ」「次へ」ボタンで、モーダルを切り替えられるようにしました。

1〜3と4〜6をそれぞれグループ化し、各グループ内でループする仕様になっています。


デモです

https://u-coded.github.io/modal/

HTML

注意点

  • トリガーボタンのdata-modal-open属性の値は、開くモーダル本体のidと一致させる
  • モーダル本体のaria-labelledbyとaria-describedbyは、それぞれモーダル内のタイトルと本文を参照させる
  • モーダル内の「前へ」ボタン (data-modal-prev) と「次へ」ボタン (data-modal-next) には、切り替えるモーダルのidを指定する。
<button type="button" data-modal-open="modal1">1つ目のモーダルを開く</button>
<button type="button" data-modal-open="modal2">2つ目のモーダルを開く</button>
<button type="button" data-modal-open="modal3">3つ目のモーダルを開く</button>
<button type="button" data-modal-open="modal4">4つ目のモーダルを開く</button>
<button type="button" data-modal-open="modal5">5つ目のモーダルを開く</button>
<button type="button" data-modal-open="modal6">6つ目のモーダルを開く</button>

<dialog id="modal1" class="modal" data-modal aria-labelledby="modal1-title" aria-describedby="modal1-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal1-title">1つ目のモーダルのタイトル</h2>
      <p id="modal1-desc">1つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal3">前のモーダルへ</button>
      <button type="button" data-modal-next="modal2">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

<dialog id="modal2" class="modal" data-modal aria-labelledby="modal2-title" aria-describedby="modal2-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal2-title">2つ目のモーダルのタイトル</h2>
      <p id="modal2-desc">2つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal1">前のモーダルへ</button>
      <button type="button" data-modal-next="modal3">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

<dialog id="modal3" class="modal" data-modal aria-labelledby="modal3-title" aria-describedby="modal3-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal3-title">3つ目のモーダルのタイトル</h2>
      <p id="modal3-desc">3つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal2">前のモーダルへ</button>
      <button type="button" data-modal-next="modal1">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

<dialog id="modal4" class="modal" data-modal aria-labelledby="modal4-title" aria-describedby="modal4-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal4-title">4つ目のモーダルのタイトル</h2>
      <p id="modal4-desc">4つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal6">前のモーダルへ</button>
      <button type="button" data-modal-next="modal5">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

<dialog id="modal5" class="modal" data-modal aria-labelledby="modal5-title" aria-describedby="modal5-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal5-title">5つ目のモーダルのタイトル</h2>
      <p id="modal5-desc">5つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal4">前のモーダルへ</button>
      <button type="button" data-modal-next="modal6">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

<dialog id="modal6" class="modal" data-modal aria-labelledby="modal6-title" aria-describedby="modal6-desc" aria-hidden="true" autofocus>
  <div class="modal-container" data-modal-container>
    <div class="modal-inner">
      <h2 id="modal6-title">6つ目のモーダルのタイトル</h2>
      <p id="modal6-desc">6つ目のモーダルの中身</p>
      <button type="button" data-modal-prev="modal5">前のモーダルへ</button>
      <button type="button" data-modal-next="modal4">次のモーダルへ</button>
      <button type="button" data-modal-close>モーダルを閉じる</button>
    </div>
  </div>
</dialog>

CSS

最低限のスタイルしか適用していないため、適宜調整してください。

dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
}

.modal {
  position: relative;
  transition: opacity 0.3s;
}

.modal::backdrop {
  transition: opacity 0.3s;
}

.modal.is-open,
.modal.is-close {
  opacity: 0;
}

.modal.is-open::backdrop,
.modal.is-close::backdrop {
  opacity: 0;
}

.modal-container {
  width: min(calc(100% - 100px), 800px);
  height: fit-content;
  inset: 0;
  margin: auto;
  position: fixed;
  top: 0;
  left: 0;
  background: white;
}

.modal-inner {
  max-height: calc(100dvh - 100px);
  padding: 50px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
}

JavaScript

// モーダルに関連するセレクターとクラス名を定義
const MODAL_SEL = "[data-modal]"; // モーダル要素を示すセレクター
const OPEN_SEL = "[data-modal-open]"; // モーダルを開くボタンのセレクター
const CONTAINER_SEL = "[data-modal-container]"; // モーダルのコンテンツを囲むコンテナのセレクター
const CLOSE_SEL = "[data-modal-close]"; // モーダルを閉じるボタンのセレクター
const PREV_SEL = "[data-modal-prev]"; // 前のモーダルに切り替えるボタンのセレクター
const NEXT_SEL = "[data-modal-next]"; // 次のモーダルに切り替えるボタンのセレクター
const OPEN_CLASS = "is-open"; // モーダルが開かれているときに付与するクラス
const CLOSE_CLASS = "is-close"; // モーダルが閉じられるときに付与するクラス

// DOM内の全てのモーダル、開閉トリガーを取得
const body = document.body;
const modals = document.querySelectorAll(MODAL_SEL);
const openTriggers = document.querySelectorAll(OPEN_SEL);
const closeTriggers = document.querySelectorAll(CLOSE_SEL);
const prevTriggers = document.querySelectorAll(PREV_SEL);
const nextTriggers = document.querySelectorAll(NEXT_SEL);

// 各モーダルを開くボタンにクリックイベントを設定
openTriggers.forEach((openTrigger) => {
  openTrigger.addEventListener("click", () => {
    const modalId = openTrigger.dataset.modalOpen; // data-modal-open属性からモーダルIDを取得
    openModal(modalId); // モーダルを開く関数を呼び出し
  });
});

// 各モーダルを閉じるボタンにクリックイベントを設定
closeTriggers.forEach((closeTrigger) => {
  closeTrigger.addEventListener("click", () => {
    const modalId = closeTrigger.closest(MODAL_SEL).id; // 閉じるボタンの親要素からモーダルIDを取得
    closeModal(modalId); // モーダルを閉じる関数を呼び出し
  });
});

// 前のモーダルを開くボタンの設定
prevTriggers.forEach((prevTrigger) => {
  prevTrigger.addEventListener("click", () => {
    const currentModal = prevTrigger.closest(MODAL_SEL);
    const prevModalId = prevTrigger.dataset.modalPrev;
    switchModal(currentModal.id, prevModalId);
  });
});

// 次のモーダルを開くボタンの設定
nextTriggers.forEach((nextTrigger) => {
  nextTrigger.addEventListener("click", () => {
    const currentModal = nextTrigger.closest(MODAL_SEL);
    const nextModalId = nextTrigger.dataset.modalNext;
    switchModal(currentModal.id, nextModalId);
  });
});

// 各モーダルに対して外部クリックやキーボードイベントを設定
modals.forEach((modal) => {
  modal.addEventListener("click", (e) => {
    const modalId = modal.id;
    // モーダルのコンテナ以外の領域をクリックした場合にモーダルを閉じる
    if (!e.target.closest(CONTAINER_SEL)) {
      closeModal(modalId);
    }
  });

  modal.addEventListener("keydown", (e) => {
    const modalId = modal.id;
    // Escapeキーが押された場合にモーダルを閉じる
    if (e.key === "Escape") {
      e.preventDefault(); // デフォルトの動作を防ぐ
      closeModal(modalId);
    }
  });
});

// モーダルを開く関数
function openModal(modalId) {
  const modal = document.getElementById(modalId); // モーダルIDに基づいてモーダル要素を取得

  // モーダルを開く前に body を inert に設定
  body.setAttribute("inert", "");

  modal.classList.add(OPEN_CLASS); // モーダルを開くクラスを追加
  modal.setAttribute("aria-hidden", "false"); // アクセシビリティ用にaria-hidden属性をfalseに設定
  modal.showModal(); // ネイティブのshowModal()でモーダルを表示

  // モーダル自体を inert の影響から除外
  modal.removeAttribute("inert");

  body.style.overflow = "hidden";
  body.style.height = "100vh";

  requestAnimationFrame(() => {
    modal.classList.remove(OPEN_CLASS); // 開いた後にOPEN_CLASSを削除
  });
}

// モーダルを閉じる関数
function closeModal(modalId) {
  const modal = document.getElementById(modalId); // モーダルIDに基づいてモーダル要素を取得
  modal.classList.add(CLOSE_CLASS); // モーダルを閉じるクラスを追加
  modal.setAttribute("aria-hidden", "true"); // アクセシビリティ用にaria-hidden属性をtrueに設定

  body.style.overflow = "";
  body.style.height = "";

  // アニメーション終了時にモーダルを閉じる処理を実行
  modal.addEventListener(
    "transitionend",
    () => {
      modal.classList.remove(CLOSE_CLASS); // 閉じるクラスを削除
      modal.close(); // ネイティブのclose()でモーダルを閉じる

      // モーダルを閉じた後、body を再操作可能に
      body.removeAttribute("inert");

      // 元の開くトリガーにフォーカスを戻す
      const openTrigger = document.querySelector(`[data-modal-open="${modalId}"]`);
      openTrigger?.focus();
    },
    { once: true } // 一度だけ実行するリスナー
  );
}

// モーダルを切り替える関数
function switchModal(currentModalId, nextModalId) {
  closeModal(currentModalId);
  openModal(nextModalId);
}