<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);
}