実践Webアクセシビリティ

アクセシブルなモーダルウィンドウの実装:キーボード操作とフォーカス管理

Tags: モーダルウィンドウ, アクセシビリティ, ARIA, キーボード操作, JavaScript, UIコンポーネント

はじめに

WebサイトやWebアプリケーションで情報を一時的に表示したり、ユーザーからの入力を求めたりする際に、モーダルウィンドウは非常に便利なUIコンポーネントです。しかし、その実装方法によっては、キーボードユーザーやスクリーンリーダーの利用を妨げ、アクセシビリティ上の大きな課題を生じさせる可能性があります。特に、キーボードトラップやフォーカスの管理は重要な考慮事項です。

本記事では、アクセシブルなモーダルウィンドウを実装するために必要なHTML構造、CSSによる表示制御、そして最も重要なJavaScriptによるキーボード操作とフォーカス管理について、具体的なコード例を交えながら解説します。

なぜモーダルウィンドウのアクセシビリティ対応が必要か

モーダルウィンドウは、コンテンツの上にオーバーレイとして表示され、ユーザーの操作を一時的に限定するUIです。この特性ゆえに、以下のようなアクセシビリティ上の問題が発生しやすくなります。

  1. キーボードトラップ: モーダルが表示されているにも関わらず、Tabキーなどで背後のコンテンツにフォーカスが移動してしまう、またはモーダル内でフォーカスが迷子になり外に出られなくなる状態です。キーボードのみで操作するユーザーにとって、これは操作不能に陥る致命的な問題となります。
  2. フォーカス管理の欠如:
    • モーダルが表示された際に、フォーカスが適切にモーダル内の最初の操作可能な要素(またはモーダルコンテナ自身)に移動しない。
    • モーダルが閉じられた際に、フォーカスがモーダルを開くトリガーとなった要素、または論理的に適切と思われる場所に戻らない。
  3. コンテキストの不明確さ: スクリーンリーダー利用者にとって、モーダルが表示されたことや、それが「ダイアログ」であるという役割、そしてその中にどのようなコンテンツが含まれているかが分かりにくい場合があります。
  4. 背景コンテンツへのアクセス: モーダルが表示されている間も、スクリーンリーダーが背景のコンテンツを読み上げてしまうことで、ユーザーが混乱することがあります。

これらの問題は、WCAG(Web Content Accessibility Guidelines)の複数の達成基準(例: 2.1.1 キーボード, 2.4.3 フォーカス順序, 4.1.2 名前、役割、値)に違反する可能性があります。アクセシブルなモーダルウィンドウを実装することは、すべてのユーザーがコンテンツにアクセスし、操作できるようにするために不可欠です。

アクセシブルなモーダルウィンドウの要件

アクセシブルなモーダルウィンドウは、最低限以下の要件を満たす必要があります。

具体的な実装手順

ここでは、シンプルなモーダルウィンドウを例に、アクセシビリティを考慮した実装手順をステップバイステップで解説します。

1. HTML構造

モーダルウィンドウの基本的なHTML構造は、モーダル本体とそれを覆うオーバーレイ(背景)で構成されます。

<!-- モーダルを開くボタン -->
<button id="openModalButton" aria-haspopup="dialog">
  利用規約を表示
</button>

<!-- モーダルウィンドウ本体 -->
<div id="modalOverlay" class="modal-overlay" role="dialog" aria-labelledby="modalTitle" aria-describedby="modalDescription" aria-modal="true" hidden>
  <div id="modal" class="modal-content" tabindex="-1"> <!-- tabindex="-1" でJSからのフォーカス可能にする -->
    <h2 id="modalTitle">利用規約</h2>
    <p id="modalDescription">
      ここに利用規約の本文が入ります。
      ここに利用規約の本文が入ります。
      ここに利用規約の本文が入ります。
    </p>
    <!-- モーダル内の操作可能な要素(例:同意ボタン、閉じるボタン) -->
    <button id="closeModalButton">同意して閉じる</button>
    <button class="close-button" aria-label="モーダルを閉じる">×</button>
  </div>
</div>

2. CSSによる表示制御

CSSでモーダルとオーバーレイのスタイルを定義し、初期状態では非表示にします。

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
  /* hidden属性の代わり、または補助として利用 */
  /* display: none; */ /* JavaScriptでクラスを切り替えて表示/非表示を制御 */
}

.modal-content {
  background-color: #fff;
  padding: 20px;
  border-radius: 5px;
  max-width: 500px;
  width: 90%;
  position: relative;
}

/* モーダルが表示されている状態のスタイル */
.modal-overlay.is-visible {
  display: flex;
}

/* 背景のスクロールを禁止 */
body.modal-open {
  overflow: hidden;
}

JavaScriptで.is-visibleクラスを.modal-overlay要素に付け外しすることで、モーダルを表示/非表示させます。また、モーダル表示時にbody要素に.modal-openクラスを付与し、背景のスクロールを禁止します。

3. JavaScriptによる実装(キーボード操作とフォーカス管理)

JavaScriptで、モーダルの開閉、フォーカス管理、ESCキーでの操作を実装します。

const openModalButton = document.getElementById('openModalButton');
const modalOverlay = document.getElementById('modalOverlay');
const modalContent = document.getElementById('modal');
const closeModalButton = document.getElementById('closeModalButton'); // 同意して閉じるボタン
const closeIcon = modalOverlay.querySelector('.close-button'); // ×ボタン

let elementBeforeModal = null; // モーダルを開く直前にフォーカスされていた要素

// フォーカス可能な要素を取得するセレクター
const focusableElementsSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

// モーダルを開く関数
function openModal() {
  // モーダルを開く直前の要素を記録
  elementBeforeModal = document.activeElement;

  // モーダルを表示
  modalOverlay.hidden = false;
  // CSSによる表示切り替えの場合はこちらを使用
  // modalOverlay.classList.add('is-visible');
  // document.body.classList.add('modal-open'); // 背景スクロール禁止

  // モーダルコンテナにフォーカスを移動
  // この後、モーダル内の最初の操作可能要素にフォーカスを移動させることが推奨される
  modalContent.focus();

  // モーダル外のコンテンツを非活性にする (aria-modal="true" の代替/補助)
  // 実際には、モーダルコンテナ以外のbody直下の子要素に aria-hidden="true" を設定するなど、より複雑な処理が必要になる場合があります。
  // ここでは簡略化のため割愛しますが、実運用では検討してください。

  // イベントリスナーを設定
  document.addEventListener('keydown', handleKeyDown);
  modalOverlay.addEventListener('click', handleOverlayClick);
}

// モーダルを閉じる関数
function closeModal() {
  // モーダルを非表示
  modalOverlay.hidden = true;
  // CSSによる表示切り替えの場合はこちらを使用
  // modalOverlay.classList.remove('is-visible');
  // document.body.classList.remove('modal-open'); // 背景スクロール許可

  // モーダルを開く直前の要素にフォーカスを戻す
  if (elementBeforeModal) {
    elementBeforeModal.focus();
    elementBeforeModal = null; // 参照をクリア
  }

  // イベントリスナーを解除
  document.removeEventListener('keydown', handleKeyDown);
  modalOverlay.removeEventListener('click', handleOverlayClick);
}

// キーボード操作(特にESCキーとTabキー)を処理
function handleKeyDown(event) {
  const isTabPressed = (event.key === 'Tab' || event.keyCode === 9);
  const isEscapePressed = (event.key === 'Escape' || event.keyCode === 27);

  // ESCキーで閉じる
  if (isEscapePressed) {
    closeModal();
    event.preventDefault(); // デフォルトのESCキー動作(ブラウザによってはページを閉じたりする)を抑制
  }

  // Tabキーによるフォーカスループ制御
  if (isTabPressed) {
    const focusableElements = modalContent.querySelectorAll(focusableElementsSelector);
    const firstFocusableEl = focusableElements[0];
    const lastFocusableEl = focusableElements[focusableElements.length - 1];

    if (event.shiftKey) { // Shift + Tab
      if (document.activeElement === firstFocusableEl) {
        lastFocusableEl.focus();
        event.preventDefault();
      }
    } else { // Tab
      if (document.activeElement === lastFocusableEl) {
        firstFocusableEl.focus();
        event.preventDefault();
      }
    }
  }
}

// オーバーレイクリックで閉じる(任意)
function handleOverlayClick(event) {
  // モーダルコンテンツ自体へのクリックでは閉じないように判定
  if (event.target === modalOverlay) {
    closeModal();
  }
}

// イベントリスナーの設定
openModalButton.addEventListener('click', openModal);
closeModalButton.addEventListener('click', closeModal);
closeIcon.addEventListener('click', closeModal);

// --- モーダル表示時のフォーカス初期位置に関する補足 ---
// modalContent.focus() でモーダルコンテナにフォーカスを当てるのが基本的な方法です。
// しかし、多くの場合、モーダル内の最初のインタラクティブな要素(ボタンや入力欄など)にフォーカスを移動させる方がユーザーにとって自然です。
// openModal 関数内で、modalContent.focus() の後に以下の処理を追加することを検討してください。
// 例:
// const focusableElements = modalContent.querySelectorAll(focusableElementsSelector);
// if (focusableElements.length > 0) {
//   focusableElements[0].focus();
// } else {
//   modalContent.focus(); // 操作可能要素がない場合はモーダルコンテナに
// }

// --- モーダル外のコンテンツ非活性化に関する補足 ---
// aria-modal="true" は多くのモダンブラウザとスクリーンリーダーでサポートされていますが、古い環境では不十分な場合があります。
// 確実にモーダル外のコンテンツを無効化するには、JavaScriptでモーダル以外の要素に aria-hidden="true" を付与したり、
// CSSでポインターイベントを無効化したりするなどの対応が必要です。
// 例えば、モーダルコンテナの兄弟要素すべてに aria-hidden="true" を設定し、モーダルが閉じられたら解除する処理などです。
// これは実装が複雑になるため、ここでは詳細なコードは割愛します。ライブラリの利用も検討してください。

JavaScriptによる主な処理の解説:

実装時の注意点やよくある落とし穴

テスト方法

実装したアクセシブルなモーダルウィンドウは、以下の方法でテストしてください。

  1. キーボード操作:
    • Tabキー、Shift+Tabキーを使って、モーダル内外を移動してみてください。モーダルが表示されている間、フォーカスがモーダル内に閉じ込められること、モーダル内の最初と最後の要素でフォーカスがループすることを確認してください。
    • ESCキーを押してモーダルが閉じられることを確認してください。
    • EnterキーやSpaceキーで、モーダルを開くボタンやモーダル内の操作可能な要素がアクティブになることを確認してください。
    • モーダルを閉じた後、フォーカスがモーダルを開くボタンに戻ることを確認してください。
  2. スクリーンリーダーでの確認:
    • VoiceOver (macOS/iOS), NVDA (Windows), JAWS (Windows) などのスクリーンリーダーを使用して、モーダルを開閉する操作を試してください。
    • モーダルが開いた際に、それがダイアログとして認識され、タイトルや説明が読み上げられることを確認してください。
    • モーダルが表示されている間、Tabキーでモーダル内の要素のみが読み上げられ、背景のコンテンツは読み上げられないことを確認してください。
    • モーダルを閉じた後、元の場所に戻って操作を再開できることを確認してください。
  3. アクセシビリティ評価ツール:
    • Lighthouse, axe DevTools, WebAIM WAVEなどの自動評価ツールを使用して、ARIA属性の誤りやその他の一般的なアクセシビリティ問題を検出してください。ただし、キーボードトラップやフォーカス順序など、ツールでは検出できない問題もあるため、手動テストは必須です。

まとめ

アクセシブルなモーダルウィンドウの実装は、単に見た目を整えるだけでなく、キーボードユーザーやスクリーンリーダー利用者を含むすべてのユーザーがWebサイトを円滑に利用できるようにするために非常に重要です。

本記事で解説したHTML構造、CSSによる表示制御、そしてJavaScriptによるキーボード操作とフォーカス管理は、アクセシブルなモーダルを実装するための基本的なステップです。特にJavaScriptによるフォーカス管理とキーボード操作のハンドリングは、多くのWebサイトで見落とされがちな部分であり、丁寧な実装が求められます。

ここで紹介したコード例を参考に、ご自身のプロジェクトに合わせたアクセシブルなモーダルウィンドウの実装に挑戦してみてください。実装後は、必ず様々な方法でテストを行い、すべてのユーザーにとって使いやすいUIとなっているかを確認することが大切です。