dialog 要素と :modal 疑似クラスでモーダルを作る

dialog 要素と :modal 擬似クラスを使用すると、アクセシブルなモーダルダイアログが非常に簡単につくることができます。Chrome 105 または Safari 15.6 以上が必要になるため実際に使用するのは難しいかもしれませんが、特定環境でのみ使用される管理画面など条件によっては使えるかもしれません。ただし iOS Safari では背景のスクロールをロックすることができないため特殊な対応が必要になります。
2022.10.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Chrome 105 から :modal 擬似クラスが使用できるようになっていたので、dialog 要素を使ったモーダルを作成してみました。

dialog 要素

dialog 要素についてですが Safari の対応が 15.4 以降になるため、実際に使用する場合には polyfill が必要になることが多いかもしれません。

モーダルをアクセシブルに実装するのは、それなりに実装コストがかかります。次のページにモーダルダイアログの実装例があります。

キーボード操作や class を切り替えるための JavaScript がかなりの行数をもって記述されていることがわかります。

しかし、dialog 要素を使えばそのような記述は不要になります。

export const App = (): JSX.Element => {
  const ref = useRef<HTMLDialogElement>(null);

  return (
    <>
      <button onClick={() => ref.current?.showModal()}>Open Modal</button>

      <dialog ref={ref}>
        <p>Hello, world!</p>

        <button onClick={() => ref.current?.close()}>Close Modal</button>
      </dialog>
    </>
  );
};

最上位のモーダルダイアログにするため、dialog.show() ではなく dialog.showModal() を使用します。これだけで ESC でダイアログを閉じたり、TAB の制御ができていることが確認できます。

dialog.showModal() で表示された dialog 要素については ::backdrop 擬似要素で Scrim の色を指定することができます。

赤い背景を持つダイアログ

dialog::backdrop {
  background: rgba(128, 0, 0, 0.5);
}

ただしこれだけでは、モーダルの背景のコンテンツにスクロールが発生していた場合にロックがされません。

:modal 擬似クラス

:modal 擬似クラスと :has 擬似クラスを使用することで、モーダルが表示されているときにだけスタイルを指定することが可能になります。:has 擬似クラスについては次の記事で解説しています(リンク先ではフラグを有効化する必要がありますと書いていますが、ちょうど同じタイミングの Chrome 105 でデフォルト有効化されています)。

次のコードを body に追加することで、ダイアログが展開したときにのみ背景にあるコンテンツのスクロールを抑止できます。

body:has(dialog:modal) {
  touch-action: none;
  -webkit-overflow-scrolling: none;
  overflow: hidden;
  overscroll-behavior: none;
}

こちらのコードは Prevent Scroll Chaining With Overscroll Behavior - Ahmad Shadeed を参考にさせていただいています。

気をつけるべき点

iOS Safari で背景のスクロールをロックできない問題

残念ながら上記のコードでは iOS Safari の背景のスクロールをロックできません。

iOS Safariでの挙動

もし iOS Safari でこの点が問題になるようであれば、dialog 要素を dialog.showModal() で top-most modal dialog にするのではなく dialog.show() を使用する必要があります。

export const App = (): JSX.Element => {
  const ref = useRef<HTMLDialogElement>(null);

  return (
    <>
      <button
        onClick={() => {
          document.body.classList.add("open");
          ref.current?.show();
        }}
      >
        Open Modal
      </button>

      <dialog ref={ref}>
        <p>Hello, world!</p>

        <button
          onClick={() => {
            document.body.classList.remove("open");
            ref.current?.close();
          }}
        >
          Close Modal
        </button>
      </dialog>
    </>
  );
};
body.open {
  touch-action: none;
  -webkit-overflow-scrolling: none;
  overflow: hidden;
  overscroll-behavior: none;
}

dialog.showModal() を使用しなかった場合、:modal 擬似クラスと ::backdrop 擬似要素を使うことができません。上記の簡易実装では backdrop がない実装になっているため注意してください。またキーボードの制御などもないため、制御のための実装も必要になります。

まとめ

dialog 要素と :modal 擬似クラスを使用すると、アクセシブルなモーダルダイアログが非常に簡単につくることができます。ただし iOS Safari では背景のスクロールをロックすることができないため特殊な対応が必要になります。

実際にはまだ使うには対応ブラウザを考えると難しいかもしれませんが、特定環境でのみ使用される管理画面など条件によっては使えるかもしれません。