dialogタグを用いた複数設置対応のモーダルをJavaScriptで実装してみる(備忘録)

今回は、dialogタグを用いた複数設置のモーダルをJavaScriptで実装してみます。もちろん単数設置で使うことも可能です。

CSSについてもできる限り、モダンな書き方で対応できるように見直しています。

それでは実装に入っていきます。

dialogタグのブラウザ対応状況について

以下のMDNのページを見てみると、最新のバージョンのブラウザには全て対応していることが確認できます。よほどのことがない限りは積極的に使っていいかと思います。

ダイアログ要素 – HTML: HyperText Markup Language | MDN

HTMLの記述について

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>dialogタグを用いた複数設置のモーダル実装
</title>
  <link rel="stylesheet" href="../reset.css">
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <div class="bl_wrapper">
    <h1>dialogタグを用いた複数設置のモーダル実装</h1>
    <div class="bl_button_inner">
      <button class="js_dialog_open el_button" data-dialog="#js_dialog_1">dialog1を開く</button>
      <button class="js_dialog_open el_button" data-dialog="#js_dialog_2">dialog2を開く</button>
      <button class="js_dialog_open el_button" data-dialog="#js_dialog_3">dialog3を開く</button>
    </div>

    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
    <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
  </div>


  <!-- dialogのコンテンツ -->
  <dialog id="js_dialog_1" class="bl_dialog">
    <div>
      <h2>dialog1のコンテンツ</h2>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <button class="js_dialog_close el_button" value="close">dialog1を閉じる</button>
    </div>
  </dialog>
  <dialog id="js_dialog_2" class="bl_dialog">
    <div>
      <h2>dialog2のコンテンツ</h2>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <button class="js_dialog_close el_button" value="close">dialog2を閉じる</button>
    </div>
  </dialog>
  <dialog id="js_dialog_3" class="bl_dialog">
    <div>
      <h2>dialog3のコンテンツ</h2>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <p class="el_text">テキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入りますテキストが入ります</p>
      <button class="js_dialog_close el_button" value="close">dialog3を閉じる</button>
    </div>
  </dialog>

  </main>
  <script src="style.js"></script>
</body>

</html>

dialogを開く部分のコードのポイントは、data-dialogの部分です。
これに#を先頭につけてjs_dialog_1、js_dialog_2、js_dialog_3と、ボタンの数分の番号を振っていきます。

<button class="js_dialog_open el_button" data-dialog="#js_dialog_1">dialog1を開く</button>
<button class="js_dialog_open el_button" data-dialog="#js_dialog_2">dialog2を開く</button>
<button class="js_dialog_open el_button" data-dialog="#js_dialog_3">dialog3を開く</button>

続いて、上記のbuttonタグに対して振った番号と同じものを、下記のdialogタグに対しidをjs_dialog_1、js_dialog_2、js_dialog_3と、ボタンと同じ数分付けていきます。これによりbuttonとdialogをJavaScriptで連動できるようになります。

<dialog id="js_dialog_1" class="bl_dialog">
  // 中身は省略しています
</dialog>
<dialog id="js_dialog_2" class="bl_dialog">
  // 中身は省略しています
</dialog>
<dialog id="js_dialog_3" class="bl_dialog">
  // 中身は省略しています
</dialog>

CSSの記述について

@charset "utf-8";

/* ==========================
  初期設定
========================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}
img {
  width: 100%;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

/* ここから実装 */

h1 {
  font-size: 24px;
  margin-bottom: 16px;
  font-weight: bold;
}
h2 {
  font-weight: bold;
  font-size: 20px;
}

body {
  padding: 80px 0;
  margin: 0;
  font-family: sans-serif;
  color: #333;
  background: #ccc;
}

.bl_wrapper {
  width: min(100%, 800px);
  margin: auto;
  padding: 0 24px;
}

.bl_button_inner {
  margin-block: 40px;
}

.el_button {
  display: grid;
  place-items: center;
  cursor: pointer;
  background: #444;
  color: #fff;
  padding: 16px;
  width: 240px;
  border-radius: 50px;
  font-size: 16px;
  margin: 24px auto 0;
  min-height: 44px;
}

.el_button:focus-visible {
  outline: solid 3px blue;
}

.el_text {
  font-size: 16px;
}

* + .el_text {
  margin-top: 16px;
}

/* dialogのコンテンツ */
.bl_dialog {
  width: min(500px, calc(100% - 48px));
  height: min(280px, calc(100% - 48px));
  background-color: #fff;
  position: fixed;
  inset: 0;
  margin: auto;
  border: 0px;
  padding: 24px;
  overscroll-behavior-y: contain;
}

.bl_dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}

ポイントを見ていきます。

widthのmin()について

例えば下記のCSSの場合は、max-widthが100%、widthが800pxを意味します。1行で済むのはいいのですが、ショートハンドがなさげですので、どうすれば効率よく書けるか模索中です。

.bl_wrapper {
  width: min(100%, 800px);
}

ボタンの上下中央配置

今はたった2行でできます。参考までに載せておきます。

.el_button {
  display: grid;
  place-items: center;
}

positionを使った上下中央配置

今は下記でいけます。厳密には色々ありますが、今回の趣旨ではありませんので、載せておく程度にとどめます。

.bl_dialog {
  position: fixed; // またはabsoluteでもOK
  inset: 0;
  margin: auto;
}

overscroll-behaviorについて

モーダルの中の要素が多い時、縦や横にスクロールバーが出て操作するようなことがあるかと思います。
この時、モーダル内のスクロールが端まで到達した後、裏側のモーダル以外のコンテンツのスクロールはどうなるでしょうか。

実はSafariの場合、端に到達したあとにスクロールすると、裏側のモーダル以外の部分がスクロールされてしまいます。

このようなときに使えるのが、overscroll-behaviorになります。今回は縦軸になりますのでoverscroll-behavior-yを使っています。

bl_dialog {
  overscroll-behavior-y: contain;
}

詳しい使い方は下記に載っていますので、見てみてください。
overscroll-behavior – CSS: カスケーディングスタイルシート | MDN

backdropについて

これはJavaScript側で、該当の要素をクリックしたときにshowModal();の記述しているときに発生するものです。

CSSでは下記のように設定していますが、これはモーダル時のモーダルの中身以外の背景色を調整できるものになります。

.bl_dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}

JavaScriptの記述について

"use strict";

// 変数定義
const dialogOpenButtons = document.querySelectorAll(".js_dialog_open");
1. const dialogCloseButtons = document.querySelectorAll(".js_dialog_close");

// 開く時
dialogOpenButtons.forEach((button) => {
  const dialog = document.querySelector(button.dataset.dialog);

  button.addEventListener("click", () => {
    dialog.showModal();
  });
});

// 閉じる時
dialogCloseButtons.forEach((button) => {
  const dialog = button.closest("dialog");

  button.addEventListener("click", () => {
    dialog.close();
  });
});

ボタンを押して開く時のclass「js_dialog_open」、開いたときのモーダルの中にあるボタンを押して閉じる時のclass「js_dialog_close」について、変数で定義します。
あとは開くときと閉じるときで、forEachで繰り返し処理をかけていきます。

厳密には違うかもしれませんが、ざっくりな開く時の場合の流れを説明しますと、

  1. foreachによりHTML上のjs_dialog_openのボタン要素が配列として格納されています
  2. js_dialog_openのボタンをクリックしたら、クリックしたボタンがjs_dialog_1と同じidがついたdialogが開くよう配列から探して取得してくれます
  3. 該当のdialogが開きます

このような流れになります。閉じるときも同じです。

以上になります。

なお、こちらはキーボード操作にも対応させていますので、tabキーやEnter/retuenキーでも操作できるかと思います。

また、詳しくはありませんが、dialogタグを用いることで、これまで別のタグを用いてモーダル対応していた場合と比べてアクセシビリティにも配慮したマークアップができるようになるようです。興味のある方は調べてみるといいかと思います。