HTMLでモーダルUIを作るときに気をつけたいこと

134
66
376

ダイアログやハンバーガーメニューといったユーザーインタフェース(UI)は、多くのウェブサイトで利用されており頻繁に見かけます。どこでも見かけることから「簡単に作成できる」と思われがちですが、意外と実装が難しいUIです。たとえば、エンジニアでなくとも、以下のような現象に気付いたことはないでしょうか?

  • ダイアログを表示中に、裏側のコンテンツがスクロールできてしまった
  • ダイアログを表示中に、Tabキーでキーボード操作を行うと裏側を操作できてしまった

▼裏側がスクロールできてしまう例

▼裏側がキーボード操作できてしまう例

これらを解決するためには、手軽な正攻法はなく、複雑なJavaScriptの制御が必要になります。本記事では、ダイアログやハンバーガーメニュー等のモーダル系のUIに存在する気付きづらい問題点と、解決方法を紹介します。ダイアログとハンバーガーメニューはそれぞれ役割の異なるUIですが、画面全域を覆うUIという意味において同種の問題が発生するので、本記事ではあわせて説明します。

よくありがちなHTMLの実装を紹介

問題点を示すために、シンプルなHTMLの作例を用意しました。ダイアログとハンバーガーメニューのデモです。それぞれのボタンをクリックすると画面全域を覆うUIが出現し、[閉じる]ボタンをクリックすることで閉じられます。

▼モーダルダイアログの表示

▼ハンバーガーメニューの表示

CSSとJSの制御として、要素に.is-showというCSSクラスを付与することで表示させています。

▼モーダルダイアログのコード例

// DOM要素の参照を取得
const modalOpenButton = document.querySelector('#js-modal-button');
const modalCloseButton = document.querySelector('#js-modal-close');
const modalOverlay = document.querySelector('#js-modal-overlay');
const modalContent = document.querySelector('#js-modal');

// 開くボタンがクリックされたらモーダルを開く
modalOpenButton.addEventListener('click', () => {
  modalContent.classList.add('is-show');
  document.body.classList.add('is-scrollLock');
});
// 閉じるボタンまたはモーダルの背景がクリックされたらモーダルを閉じる
const closableElement = [modalCloseButton, modalOverlay];
closableElement.forEach((element) => {
  element.addEventListener('click', () => {
    modalContent.classList.remove('is-show');
    document.body.classList.remove('is-scrollLock');
  });
});

モーダルの表示中は<body>要素にスタイルoverflow: hiddenを設定し、マウスホイールやタッチ操作によるスクロールを無効化しています。

一見、問題なく動作しているように見えますが、以下の2つの課題があります。先ほどの作例をブラウザで開きながら問題点を確認していきましょう。

  • 課題1: iOS Safariで裏側がスクロールされる現象
  • 課題2: 裏側にキーボードフォーカスされる現象

課題1: iOS Safariで裏側がスクロールされる現象

スタイルoverflow: hiddenでスクロールを固定していても、iOS Safariでは以下のような特定のタイミングで裏側のコンテンツがスクロールできてしまいます

  • 画面下部のタブバーの表示が切り替わるタイミング
  • 最前面のコンテンツを上下どちらかにスクロールしきった後に、もう一度スクロールしたタイミング

▼モーダルダイアログの裏側がスクロールされる様子

▼ハンバーガーメニューの裏側がスクロールされる様子

この挙動は、モーダルダイアログ内にスクロールコンテンツが存在する場合に弊害があります。ページ全体のスクロール挙動に影響をうけ、モーダルダイアログ内でのスクロールができなくなります。モーダルダイアログ内にスクロールコンテンツがない場合は問題になりませんが、UIの制約が生まれてしまうので対策を検討したいところです。

この課題を対策した作例

この課題を対策するには、以下の方針が考えられます。

  • スクロールをさせたくない要素にはEventpreventDefault()stopPropagation()メソッドでスクロール挙動を抑制
  • スクロール対象の要素を上下どちらかにスクロールしきった時に、スクロール量を微調整

この方針で対策した作例は以下の通りです。

▼モーダルダイアログの裏側を固定する例

▼メニューの裏側を固定する例

具体的なJavaScriptの実装は次のリンク先から参照ください。

課題2: 裏側にキーボードフォーカスされる現象

キーボード操作を行うと、モーダルの裏側のコンテンツにフォーカスがあたってしまう問題があります。モーダルダイアログの表示中は背面が操作できないような表示になりますが、Tabキーでフォーカスを移動できています。モーダルダイアログの表示中に裏側のボタンや入力欄の操作ができるため、意図しない動作の起きるリスクが考えられます。

もう1つ関連した問題があります。モーダルダイアログの実装においては、z-indexの重なり順の対策もかねて<body>要素の末尾に表示用の要素が置くことあります。その実装をすると、モーダルダイアログの表示直後にフォーカスがすぐに当たらないという現象も発生します。

モーダルを開くボタンとダイアログのDOM要素の間に、フォーカス可能なDOM要素が存在するため、フォーカスがモーダルダイアログでない場所にあたってしまいます。

この課題を対策した作例

これを対策するにはkeydownイベントのケアが必要となります。作例とコードを示すので、詳細を知りたい方はぜひ参考ください。

対応方針

  • スクリーンリーダー向けのWAI-ARIAウェイ・アリア対応
  • keydownイベントでのフォーカスの制御(該当コード

コラム: <dialog>要素でモーダルダイアログを実装する

キーボードフォーカスやスクリーンリーダーをケアする別の方法として、<dialog>要素でモーダルダイアログを実装する方法があります。<dialog>要素はopen()close()など、ダイアログの開閉に必要なJavaScriptのメソッドが備わっています。

以下のデモは<dialog>要素を利用した実装例です。

<dialog>要素はブラウザ標準の仕様です。<dialog>要素で実装すると、キーボードフォーカスやスクリーンリーダーの挙動も手軽にケアできます。<dialog>要素は主要なブラウザの最新版であれば利用可能です。ただし、Safari 15.4以上(2022年3月リリース)で利用可能のため、それ以前のバージョンのブラウザを考慮する場合は別の方法を検討する必要があります。

注意点として、<dialog>要素はキーボードフォーカスやスクリーンリーダーの対策に利用できますが、前述したiOS Safariのスクロール挙動の対策には効果がありません。2022年現在は<dialog>要素を使いつつ、iOS Safariのスクロール対策は自前のJavaScript実装がよさそうです。

まとめ

モーダル系のUIでの「裏側のコンテンツ」に関する注意点を本記事で紹介しました。「スクロールの制御」「フォーカスの制御」の両方をケアしないと良いモーダルUIの挙動にはなりません。「ささいなことだから、対策しなくてもいいのでは・・・」となりがちな挙動かもしれませんが、ユーザビリティーやウェブアクセシビリティの観点で改善できれば理想的です。本記事がこれらの問題をケアするために参考となれば幸いです。