0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モーダル実装は自作? <dialog>?メリット・デメリットを比較してみる

0
Last updated at Posted at 2026-10-15

はじめに

普段の仕事では、PHPを中心としたWebアプリケーションやWebサイトの開発に携わっています。

  • PHP
  • TypeScript / JavaScript
  • SCSS
  • Vite

ReactやVueなどを全面的に採用するのではなく、HTML・CSS・JavaScriptを中心にUIを実装することも多いです。

その中でも、検索フォームや確認画面、お知らせなどをモーダルで表示する機会はよくあります。

モーダルというと、<div> などの要素にCSSでスタイルを当て、JavaScriptで表示・非表示を切り替える実装を思い浮かべる方も多いのではないでしょうか。

一方、HTMLにはダイアログを表現するための <dialog> 要素が用意されています。

最近は自分も <dialog> を使うことが増え、次のような点が便利だと感じています。

  • z-index の重なり順を細かく考える機会を減らせる
  • 背景部分を ::backdrop で実装できる
  • Escキーによるクローズなど、ブラウザ標準の挙動を利用できる
  • モーダルの開閉方法を統一しやすい

最近はAIを使ってコードを書くことも増えましたが、ブラウザ標準の機能を知っておくことは変わらず大切だと思っています。

生成されたコードが必要以上に複雑になっていないか、自分で判断するためにも役立つからです。

この記事では、

  • <div> などを使って自作するモーダル
  • HTMLの <dialog> を使ったモーダル

をできるだけ近い構成で比較しながら、実装の違いや使っていて気づいた点を紹介します。

掲載するコードは、違いを伝えるために簡略化したサンプルです。実際の業務コードをそのまま掲載したものではありません。

こんな方におすすめです

  • HTML・CSS・JavaScriptを使ってUIを実装することが多い方
  • モーダルを <div> などで自作したことがある方
  • <dialog> は知っているものの、実際にはあまり使ったことがない方
  • ブラウザ標準の機能を活用してUI実装をシンプルにしたい方
  • モーダルの実装方法をプロジェクト内で統一したい方

そもそもモーダルとは?

モーダルとは、画面の上に別のUIを表示し、その操作が完了するまで背後にあるコンテンツの操作を制限するUIです。

例えば、

  • 検索フォーム
  • データ削除などの確認画面
  • お知らせ機能
  • 重要なメッセージをページ読み込み時に表示する
  • フォームの入力・編集

などがあります。

一見すると「画面の上に要素を表示するだけ」に見えますが、実際には、

  • どの要素より前面に表示するか
  • 背景をどのように暗くするか
  • モーダル表示中に背景を操作させるか
  • 背景クリックで閉じるか
  • Escキーで閉じるか
  • モーダルの開閉方法をどのように統一するか

など、意外と考えることがあります。

まず <div> で作った場合を見てから、近い構造の <dialog> 版と比べていきます。


<div> を使ってモーダルを自作する

まずは、<div> を使ってモーダルを作る場合です。

後ほど紹介する <dialog> 版と比較しやすいように、HTMLの構成はできるだけ同じ形にしています。

<button class="js-buttonOpen" data-dialog="deleteModal">
  削除する
</button>

<div
  class="js-dialogModal dialogModal -delete"
  id="deleteModal"
>
  <div class="js-dialogModalWrap dialogModal__wrap">
    <div class="dialogModal__header">
      <p class="dialogModal__ttl">
        本当に削除しますか?
      </p>
    </div>

    <div class="dialogModal__body">
      <button class="js-cancel">
        キャンセル
      </button>

      <button class="js-delete">
        削除する
      </button>
    </div>
  </div>
</div>

開くボタンの data-dialog には、対象となるモーダルの id を指定しています。

data-dialog="deleteModal"

対応するモーダルには、

id="deleteModal"

を設定します。

また、モーダルの中身を .js-dialogModalWrap で囲っています。

これは、背景部分をクリックしたときにはモーダルを閉じ、モーダルのコンテンツ内をクリックした場合は閉じないようにするための判定にも利用できます。

CSSでは、モーダルを画面全体に固定し、重なり順や背景を指定します。ここでの .dialogModal のCSSは <div> 版専用の例です。後述する <dialog> 版には display: none をそのまま適用しません。

.dialogModal {
  position: fixed;
  inset: 0;
  z-index: 1000;

  display: none;
  place-items: center;

  background: rgb(0 0 0 / 50%);
}

.dialogModal.is-open {
  display: grid;
}

.dialogModal__wrap {
  background: #fff;
}

JavaScriptでは、data-dialog の値から対象のモーダルを取得し、クラスを付け外しすることで開閉します。

const openButton = document.querySelector(".js-buttonOpen");

const modalId = openButton.dataset.dialog;
const modal = document.getElementById(modalId);

const modalWrap = modal.querySelector(".js-dialogModalWrap");
const cancelButton = modal.querySelector(".js-cancel");

openButton.addEventListener("click", () => {
  modal.classList.add("is-open");
});

cancelButton.addEventListener("click", () => {
  modal.classList.remove("is-open");
});

modal.addEventListener("click", (event) => {
  if (modalWrap.contains(event.target)) {
    return;
  }

  modal.classList.remove("is-open");
});

.js-dialogModalWrap の中をクリックした場合は何もせず、その外側をクリックした場合のみモーダルを閉じています。

表示・非表示だけなら、このように比較的簡単に実装できます。ただし、このサンプルだけではキーボード操作やフォーカスの移動・復帰、背景操作の制限などは再現できていません。実際のモーダルとして使う場合には、そうした点にも対応が必要です。

また、自作モーダルの大きなメリットは、細かい挙動まで自分で制御できることです。

表示位置やアニメーション、背景、閉じるタイミング、重なり順などを、必要に応じて調整できます。

特殊なデザインや独自の挙動が必要な場合には、自作の方が扱いやすいケースもあります。

自由度が高いからこその難しさ

一方で、自由度が高いということは、管理するものも増えるということです。

例えば、モーダルを他の要素より前に表示したい場合、

.dialogModal {
  z-index: 1000;
}

のような指定をすることがあります。

しかし、プロジェクトが大きくなってくると、

.header {
  z-index: 100;
}

.menu {
  z-index: 500;
}

.dialogModal {
  z-index: 1000;
}

.anotherModal {
  z-index: 9999;
}

のように、さまざまな場所で z-index が使われ始めることがあります。

さらに別の画面で、

z-index: 99999;

のような指定が追加されていくと、

なぜこの値になっているのか?

どの要素より上に表示したかったのか?

が分かりにくくなってきます。

小規模なWebサイトであればそれほど問題にならなくても、長期間運用するWebアプリケーションや、多くのファイルを扱うプロジェクトでは管理が難しくなることがあります。

また、自作の場合はモーダルごとに実装方法を自由に決められるため、

  • Aのモーダルは背景クリックで閉じる
  • BのモーダルはEscキーで閉じる
  • Cのモーダルは独自のJavaScriptで開閉する

といったように、実装ルールがばらつく可能性もあります。


<dialog> を使ってモーダルを作る

次に、先ほどとほぼ同じ構成を <dialog> を使って実装してみます。

<button class="js-buttonOpen" data-dialog="deleteModal">
  削除する
</button>

<dialog
  class="js-dialogModal dialogModal -delete"
  id="deleteModal"
>
  <div class="js-dialogModalWrap dialogModal__wrap">
    <div class="dialogModal__header">
      <p class="dialogModal__ttl">
        本当に削除しますか?
      </p>
    </div>

    <div class="dialogModal__body">
      <button class="js-cancel">
        キャンセル
      </button>

      <button class="js-delete">
        削除する
      </button>
    </div>
  </div>
</dialog>

<div> 版との大きな違いは、モーダル本体が、

<div>

ではなく、

<dialog>

になったことです。

JavaScriptでは、モーダルを開くために showModal()、閉じるために close() を使用します。

const openButton = document.querySelector(".js-buttonOpen");

const dialogId = openButton.dataset.dialog;
const dialog = document.getElementById(dialogId);

const dialogWrap = dialog.querySelector(".js-dialogModalWrap");
const cancelButton = dialog.querySelector(".js-cancel");

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

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

dialog.addEventListener("click", (event) => {
  if (dialogWrap.contains(event.target)) {
    return;
  }

  dialog.close();
});

自作版では、

modal.classList.add("is-open");

としていた部分が、

dialog.showModal();

になります。

閉じる場合も、

modal.classList.remove("is-open");

ではなく、

dialog.close();

を利用します。

背景クリックの判定については自作版とほぼ同じ考え方で、.js-dialogModalWrap の中でクリックされた場合は何もせず、それ以外の場合のみ close() を実行しています。

HTMLの構造やクリック判定の考え方は大きく変えずに、モーダルとしての開閉をブラウザ標準の仕組みに置き換えることができます。


<dialog> を使うメリット

Top Layerに表示される

個人的に <dialog> を使っていて特に便利だと感じるのが、Top Layerに表示されることです。

showModal() で表示した <dialog> は、通常のDOM上の重なり順とは別に、ブラウザが管理するTop Layerへ表示されます。

そのため、

z-index: 999999;

のように大きな値を設定して、

このモーダルは他の要素より上に表示されるだろうか?

と考える機会を減らすことができます。

自作モーダルでは z-index を自由に設定できる反面、その重なり順も自分で管理する必要があります。

<dialog> では、その部分をブラウザ側に任せられるのが大きなメリットです。


::backdrop が使える

モーダルを表示すると、背景を半透明の黒などで覆うことが多いと思います。

<dialog> を showModal() で表示した場合は、::backdrop 疑似要素を利用できます。

.dialogModal::backdrop {
  background: rgb(0 0 0 / 50%);
}

例えばダイアログ本体は、

.dialogModal {
  padding: 0;
  border: 0;
}

としておき、背景部分だけを ::backdrop でスタイリングできます。

自作モーダルでは、モーダル本体や別の背景用要素に背景色を設定するケースがありますが、<dialog> では背景部分を専用の疑似要素として扱えます。


Escキーで閉じる処理が標準で用意されている

showModal() で表示した <dialog> は、Escキーによるクローズに標準で対応しています。

自作の場合、同じ挙動を実装するのであれば、

document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    modal.classList.remove("is-open");
  }
});

のような処理を追加することがあります。

<dialog> では、こうした基本的な挙動をブラウザ側に任せることができます。

ただし、この挙動はメリットである一方、要件によっては注意が必要です。

こちらについては後ほど紹介します。


背景の操作を制限できる

showModal() で開いた <dialog> では、ダイアログ以外のページコンテンツが操作できない状態になります。

自作の場合は、モーダルを画面全体に表示しただけでは、背景側の要素に対する操作についても考える必要があります。

<dialog> をモーダルとして利用すると、こうしたモーダルの基本的な仕組みの一部をブラウザ側に任せることができます。


開閉方法を統一しやすい

実務で使っていてもう一つ便利だと感じるのが、開閉方法を統一しやすいことです。

モーダルを開く場合は、

dialog.showModal();

閉じる場合は、

dialog.close();

という共通の方法があります。

自作の場合は、

classList.add("is-open");

を使うのか、

style.display = "block";

を使うのかなど、プロジェクト側でルールを決める必要があります。

もちろん自作でもルールを決めて共通化できますが、<dialog> では最初から開閉用のAPIが用意されているため、それを基準に実装を揃えやすくなります。


<dialog> を使うときに気をつけたいこと

ここまでを見ると、

モーダルなら全部 <dialog> にすればよいのでは?

と思うかもしれません。

実際に使ってみると便利な機能は多いですが、ブラウザ標準の挙動があるからこそ注意したい部分もあります。


Escキーで閉じてほしくない場合は対策が必要

先ほどメリットとして紹介したEscキーによるクローズですが、モーダルによっては閉じてほしくないケースもあります。

例えば、

  • 必ず確認してほしい重要なメッセージ
  • 特定の操作を完了するまで閉じてほしくない画面

などです。

その場合は、cancel イベントでデフォルトの挙動を止めることができます。

dialog.addEventListener("cancel", (event) => {
  event.preventDefault();
});

ブラウザ標準の挙動を利用できるのは便利ですが、

標準ではどのように動くのか?

を理解したうえで、要件に合わせて調整する必要があります。


背景クリックで閉じる場合は、クリック範囲を判定する

今回のサンプルでは、モーダルのコンテンツ部分を .js-dialogModalWrap で囲っています。

<dialog class="js-dialogModal dialogModal" id="deleteModal">
  <div class="js-dialogModalWrap dialogModal__wrap">
    <!-- モーダルのコンテンツ -->
  </div>
</dialog>

JavaScriptでは、クリックされた場所が .js-dialogModalWrap の内側かどうかを判定します。

const dialogWrap = dialog.querySelector(".js-dialogModalWrap");

dialog.addEventListener("click", (event) => {
  if (dialogWrap.contains(event.target)) {
    return;
  }

  dialog.close();
});

モーダルのコンテンツ内をクリックした場合は何もせず、その外側をクリックした場合のみ close() を実行します。

例えば、入力フォームを操作したり、モーダル内のボタンを押したりしたときに意図せず閉じることを防ぐためです。

このように、モーダル本体とコンテンツ部分を分けておくことで、背景クリックによるクローズなどの処理も共通化しやすくなります。ただし、<dialog> 自身の余白をクリックした場合も対象になるため、実際のデザインに合わせた判定は必要です。


show() と showModal() は違う

<dialog> には、

dialog.show();

と、

dialog.showModal();

があります。

似ていますが、動作は異なります。

show() は非モーダルなダイアログとして表示します。

一方、

dialog.showModal();

はモーダルダイアログとして表示します。

今回の記事で扱っているような、背景の操作を制限するモーダルの場合は showModal() を利用します。

初めて <dialog> を使う場合には、少し注意したいポイントです。


クラス化するとさらに使い回しやすい

ここまでのサンプルでは、ひとつのモーダルだけを対象にしています。

しかし実務では、

  • 削除確認
  • 検索フォーム
  • お知らせ
  • 編集フォーム

など、さまざまな場所でモーダルを使用します。

そのたびに、

const openButton = ...
const dialog = ...
const dialogWrap = ...
const cancelButton = ...

と個別に処理を書くのは大変です。

そこで、モーダルの処理をクラス化して共通化しておくと扱いやすくなります。

例えば、開くボタンには共通で、

class="js-buttonOpen"

を指定し、data-dialog に開きたいモーダルの id を設定します。

<button
  class="js-buttonOpen"
  data-dialog="deleteModal"
>
  削除する
</button>

別のモーダルであれば、

<button
  class="js-buttonOpen"
  data-dialog="searchModal"
>
  検索する
</button>

のように指定できます。

JavaScript側では、例えば次のように共通化できます。イベントは初期化時に一度登録し、開くたびにリスナーが増えないようにしています。

class DialogModal {
  constructor() {
    this.init();
  }

  init() {
    document.querySelectorAll(".js-dialogModal").forEach((dialog) => {
      if (!(dialog instanceof HTMLDialogElement)) return;

      const wrap = dialog.querySelector(".js-dialogModalWrap");
      const cancelButton = dialog.querySelector(".js-cancel");

      cancelButton?.addEventListener("click", () => dialog.close());

      dialog.addEventListener("click", (event) => {
        if (event.target === dialog && !wrap?.contains(event.target)) {
          dialog.close();
        }
      });
    });

    document.querySelectorAll(".js-buttonOpen").forEach((button) => {
      button.addEventListener("click", () => {
        const dialogId = button.dataset.dialog;
        const dialog = document.getElementById(dialogId);

        if (dialog instanceof HTMLDialogElement && !dialog.open) {
          dialog.showModal();
        }
      });
    });
  }
}

new DialogModal();

実際に共通化していくなら、

  • Escキーで閉じる・閉じないを設定する
  • 背景クリックで閉じる・閉じないを設定する
  • モーダルを開いたときに処理を実行する
  • 閉じたときに処理を実行する
  • モーダルごとのオプションを持たせる

といった機能を追加できます。

共通のクラスとして管理しておけば、各ページではHTML側で対象となるモーダルを指定するだけで利用できるようになります。

モーダルごとに個別のJavaScriptを書く必要が減るため、実装ルールの統一にもつながります。


自作モーダルと <dialog> を比較する

ここまでの内容を簡単に整理してみます。

項目 自作モーダル <dialog>
デザインの自由度 高い 高い
独自の挙動 自由に実装しやすい 標準挙動を理解したうえで調整
重なり順 z-index などを自分で管理 Top Layer
背景 自分で実装 ::backdrop が使える
Escキー 自分で実装 標準で対応
背景操作の制限 自分で実装 showModal() でブラウザが対応
開閉処理 プロジェクト側で決める showModal() / close()
実装ルール 自由な分、ばらつく可能性がある 一定のルールに揃えやすい
特殊なUI 柔軟に対応しやすい 標準仕様に合わせる必要がある

どちらかが完全に優れているというより、それぞれ特徴があります。


自作モーダルのメリット・デメリット

メリット

自作モーダルの一番のメリットは、自由度の高さです。

HTML構造からCSS、JavaScriptまで自由に組み合わせられるため、

  • 特殊なアニメーション
  • 独自の表示位置
  • 独自の閉じ方
  • 複雑なデザイン

などにも柔軟に対応できます。

また、z-index を含めた表示順についても、プロジェクト独自のルールで制御できます。

すでにプロジェクト内に自作モーダルの仕組みが確立されていて、ルールも統一されているのであれば、その仕組みを使い続ける選択肢もあります。

デメリット

一方で、自由度が高い分、管理する範囲も増えます。

特に長期間運用しているプロジェクトでは、

z-index: 1000;
z-index: 9999;
z-index: 99999;

といった値がさまざまな場所に追加され、重なり順のルールが分かりにくくなることがあります。

また、モーダルごとに異なるJavaScriptを書くようになると、ファイルが増えたときに、

このモーダルはどの処理で動いているのか?

を追うことも難しくなります。

自由に作れるからこそ、プロジェクト側で明確なルールを作る必要があります。


<dialog> のメリット・デメリット

メリット

<dialog> のメリットは、モーダルとして必要になる基本的な仕組みをブラウザ標準の機能として利用できることです。

例えば、

  • Top Layerへの表示
  • ::backdrop
  • Escキーによるクローズ
  • 背景コンテンツの操作制限
  • showModal() / close() による開閉

などがあります。

自分で管理するものを減らせるため、実装をシンプルに保ちやすくなります。

また、共通クラスと組み合わせることで、複数の画面で同じルールを利用しやすいのもメリットです。

デメリット

一方で、ブラウザ標準の動作を理解したうえで実装する必要があります。

例えばEscキーで閉じる動作も、要件によっては止める必要があります。

また、

dialog.show();

と、

dialog.showModal();

の違いなど、<dialog> 固有の仕様について最初に理解しておく必要があります。

自作モーダルのようにすべてを自由に設計するというより、

ブラウザが用意している仕組みを利用し、その上に必要な機能を追加していく

という考え方になります。


モーダルを作るなら、まず <dialog> を検討してみる

自作モーダルと <dialog> を比較すると、どちらにもメリットがあります。

特殊なデザインや独自の挙動が多い場合には、自作モーダルの方が扱いやすいこともあります。

一方で、一般的なモーダルであれば、<dialog> を利用することで自分たちが管理する処理を減らすことができます。

個人的に実務で特に便利だと感じているのは、

ブラウザ標準の仕組みに寄せることで、プロジェクト内の実装ルールを統一しやすくなること

です。

自作の場合は自由に実装できる反面、人やページによって実装方法が変わる可能性があります。

<dialog> をベースにして、

dialog.showModal();

で開き、

dialog.close();

で閉じるという基本ルールを作っておけば、そこから共通クラスとして拡張できます。

また、.js-dialogModalWrap のように「モーダルのコンテンツ部分」を共通の構造として決めておけば、

  • コンテンツ外をクリックしたら閉じる
  • コンテンツ内のクリックは無視する

といった処理についても、モーダルごとに書き直す必要がなくなります。

特に z-index のような、プロジェクトが大きくなるほど管理が難しくなりやすい部分をブラウザ側に任せられるのは、実務では大きなメリットだと感じています。

新しくモーダルを実装するなら、最初から <div> で作り始めずに、

このモーダルは <dialog> で実現できないか?

と一度検討してみるのもよいのではないでしょうか。


まとめ

今回は、自作モーダルと <dialog> を使ったモーダルを、できるだけ同じ構成で比較してみました。

自作モーダルは自由度が高く、特殊なUIや独自の動作にも対応しやすいメリットがあります。

一方で、プロジェクトが大きくなるにつれて、

  • z-index の管理
  • モーダルごとのJavaScript
  • 開閉方法の違い
  • 実装ルールのばらつき

などを管理する必要があります。

<dialog> を利用すると、

  • Top Layer
  • ::backdrop
  • Escキーによるクローズ
  • 背景操作の制限
  • showModal() / close()

といったブラウザ標準の仕組みを利用できます。

もちろん、<dialog> を使えば何も考えなくてよいわけではありません。

Escキーで閉じてほしくない場合の対応や、背景クリック時の判定、show() と showModal() の違いなど、仕様を理解したうえで実装する必要があります。

それでも、最初からすべてを自作するのではなく、ブラウザがすでに持っている機能を利用し、その上でプロジェクトに必要な処理を追加していくことで、コードや実装ルールをシンプルに保ちやすくなります。

モーダルを新しく実装する機会があれば、選択肢のひとつとして <dialog> を試してみてはいかがでしょうか。


元記事はこちら

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?