虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

dialog 要素 + React でモーダルダイアログ実装するときの実装ポイント3選

こんにちは、虎の穴ラボの後藤です。

モーダルダイアログはユーザーに重要な情報を提供したり、決定を求めたりする際に適した UI です。

m3.material.io

重要な UI である一方で、フォーカス制御やキーボード操作、スタイリング、背景要素の非活性など考慮すべき点は多く実装も複雑になりがちです。

そこでこの記事では、HTMLネイティブのdialog要素を用いたシンプルなモーダルダイアログ実装を紹介します。 また、実際に試す中で気づいた実装のポイントについても紹介します。

dialog要素の基本

developer.mozilla.org

モーダルを開く/閉じる

<!DOCTYPE html>
<html lang="ja">
  <body>
    <div id="app">
      <button id="showModalBtn" type="button">Show Modal</button>
      <dialog id="sampleModal">
        <h1>HELLO MODAL !!</h1>
        <button id="closeModalBtn" type="button">Close Modal</button>
      </dialog>
    </div>

    <script>
      const showModalBtn = document.getElementById("showModalBtn"); // 表示ボタン
      const closeModalBtn = document.getElementById("closeModalBtn"); // 閉じるボタン

      // モーダル本体
      const sampleModal = document.getElementById("sampleModal");

      // 表示ボタンをクリック時したとき、
      showModalBtn.addEventListener("click", () => {
        // モーダルを開く
        sampleModal.showModal();
      });

      // 閉じるボタンをクリック時したとき、
      closeModalBtn.addEventListener("click", () => {
        // モーダルを閉じる
        sampleModal.close();
      });
    </script>
  </body>
</html>

dialog要素のメリット

モーダルダイアログの実装にHTMLネイティブのdialog要素を利用すると下記のようなメリットがあります。

  • 開く/閉じるをシンプルに実装できる
  • z-indexなしで最前面に表示される
  • 背景要素のスタイリングは::backdrop擬似要素を用いて設定できる

::backdrop - CSS: カスケーディングスタイルシート | MDN

  • アクセシビリティの設定はブラウザが提供する

<dialog>: ダイアログ要素 - HTML: ハイパーテキストマークアップ言語 | MDN

ブラウザのサポート状況

記事執筆時点(2024/3/14)では、主要なブラウザの最新版であれば利用可能です。

Dialog element | Can I use... Support tables for HTML5, CSS3, etc

dialog 要素 + React の実装

基本的には、特に難しい点もなくdialog要素に紐づくrefを用意し同様の操作をするだけです!

import { useRef } from "react";

export const DialogSample = () => {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const handleShowModal = () => dialogRef.current?.showModal();
  const handleCloseModal = () => dialogRef.current?.close();
  return (
    <>
      <button type="button" onClick={handleShowModal}>
        Show Modal
      </button>
      <dialog ref={dialogRef}>
        <h1>HELLO MODAL !!</h1>
        <button type="button" onClick={handleCloseModal}>
          Close Modal
        </button>
      </dialog>
    </>
  );
};

実装のポイント

1. dialog要素は閉じても DOM に存在し続ける

はじめに、dialog要素の表示状態はopen属性で管理されます。

showModal()メソッドによって表示するとdialog要素にopen属性が付与され、close()メソッドによって閉じるとopen属性は削除されます。 そのため、モーダルダイアログを開いても閉じてもopen属性が変わるだけで、dialog要素は DOM に残ったままです。

React における実装の注意点の一つとしては、close()メソッドを呼び出した際にアンマウントが行われないことが挙げられます。 したがって「閉じた際に処理を行う」といった場合はアンマウントではなく、dialog要素のcloseイベントで扱うと良さそうです。

HTMLDialogElement: close イベント - Web API | MDN

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;

    const handleCancel = () => {
      console.log("Dialog close event");
      // 「閉じた際に背景要素のスクロールを許可する」といった処理はここで行う
    };

    dialog.addEventListener("close", handleCancel);
    return () => {
      dialog.removeEventListener("close", handleCancel);
    };
  }, []);

※ 補足ですがopen属性は手動で削除すると予期せぬ動作になる可能性が高いので、読み取りのみの使用が推奨されます

https://html.spec.whatwg.org/multipage/interactive-elements.html#attr-dialog-open

2. ESC キーでモーダルダイアログを閉じる動作が提供される

アクセシビリティの観点では、モーダルダイアログは ESC キーで閉じることが一般的です。

www.w3.org

showModal()メソッドを使用すると、この動作はブラウザーが提供してくれます。

注意点としては、「閉じる際に必ず行うべき処理」があった際に、この動作を知っておかないと実装が漏れてしまう点です。

対応としては、ESC キーで閉じる際もcloseイベントが発火するので、こちらもcloseイベントで扱うと良さそうです。

3. showModal()メソッドで開くとdialog要素は Top layer に配置される

Top Layer に配置された要素は他のどのコンテンツよりも最前面に表示されます。

developer.mozilla.org

dialog要素はshowModal()メソッドで開くと Top Layer に配置され、これにより最前面に表示されるようになります。 ※ z-indexによるレイヤー調整は不要になります

実装の注意点としては、Top layer に配置されても DOM 構造は変わらない点です。

例えば次のようなスタイル指定があった時、DOM 構造は変わらないのでdialog要素内のh1の色も変わってしまいます。

// スタイリング
#container h1 {
  color: aquamarine;
}

// React コンポーネント
function App() {
  return (
    <div id="container">
      <dialog>
        <h1>HELLO MODAL !!</h1>
      </dialog>
    </div>
  );
}

この状況が望ましくないものの、他処理の関係でコンポーネントの記述を変えられない場合、React ではcreatePortalを使うことで DOM 内の別部分へレンダーすることが可能です。

ja.react.dev

  return (
    <>
      <button type="button" onClick={handleShowModal}>
        Show Modal
      </button>
      {/* body 直下にレンダリングする */}
      {createPortal(
        <dialog ref={dialogRef}>
          <h1>HELLO MODAL !!</h1>
          <button type="button" onClick={handleCloseModal}>
            Close Modal
          </button>
        </dialog>,
        document.body
      )}
    </>
  );

まとめ

dialog要素を利用するとシンプルにモーダルダイアログを実装できます。また主要な動作をブラウザが提供してくれる点も大きなメリットです。モーダルダイアログ実装の際は検討してみてください!

また、実装のポイントとして紹介しきれなかったものとして、多くの場面で必要になる「背景要素のスクロール制御」があります。下記の記事で詳しく紹介されていますので参照ください。

ics.media

Fantia開発採用情報

虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp