実践Webアクセシビリティ

アクセシブルなナビゲーションメニューの実装:キーボード操作とARIA属性、レスポンシブ対応

Tags: Webアクセシビリティ, ナビゲーション, ARIA属性, キーボード操作, レスポンシブ

はじめに

ウェブサイトのナビゲーションメニューは、ユーザーがサイト内を移動するために不可欠な要素です。特にモバイルファーストやレスポンシブデザインが主流となった現在、いわゆる「ハンバーガーメニュー」に代表されるような、表示・非表示を切り替えるタイプのナビゲーションが多く使用されています。

しかし、これらのメニューがマウス操作のみに依存している場合、キーボードユーザーやスクリーンリーダーユーザーはサイト内のコンテンツに効果的にアクセスできなくなる可能性があります。Webアクセシビリティの観点から、すべてのユーザーが容易にナビゲーションを利用できるよう、適切な実装が求められます。

この記事では、ナビゲーションメニュー、特にレスポンシブ対応で表示・非表示を切り替えるタイプのメニューをアクセシブルにするための具体的な実装方法について、キーボード操作、ARIA属性、そしてJavaScriptによる制御に焦点を当てて解説します。

なぜアクセシブルなナビゲーションが必要か

アクセシブルなナビゲーションメニューは、多様なユーザーにとってサイトの使いやすさを向上させます。

適切なHTML構造、ARIA属性、そしてJavaScriptによる制御を組み合わせることで、これらのユーザーもスムーズにサイトを探索できるようになります。

実装の基本方針

アクセシブルなナビゲーションメニューを実装するための基本的な要素は以下の通りです。

  1. セマンティックHTML: ナビゲーション領域には<nav>要素を使用し、メニュー項目はリスト(<ul><ol>)とリストアイテム(<li>)、リンク(<a>)で構成します。メニューの開閉を制御する要素には<button>を使用します。
  2. キーボード操作: Tabキーによる要素間の移動、Enter/Spaceキーによるボタンの操作、Escキーによるメニューの閉鎖など、キーボードでのすべての操作を可能にします。
  3. ARIA属性: 要素の状態(開閉など)や関連性(どのボタンがどのメニューを制御するか)を支援技術に伝えるために、ARIA属性(aria-expanded, aria-controls, aria-haspopupなど)を適切に使用します。
  4. フォーカス管理: メニューが開閉した際に、ユーザーのフォーカスが適切な位置に移動するように制御します。例えば、メニューが開いたらメニュー内の最初の項目にフォーカスを移す、メニューを閉じたらトグルボタンに戻す、といった挙動です。
  5. 表示・非表示の制御: CSSのdisplay: nonevisibility: hidden、あるいはHTML要素の属性(例: hidden属性)を使用して、非表示時の要素がアクセシビリティツリーから除外されるようにします。単純なCSSによる位置指定や透明度による非表示は避けてください。

具体的な実装手順

ここでは、一般的なレスポンシブナビゲーション(デスクトップでは常に表示、モバイルではトグルボタンで表示/非表示)を例に実装手順を解説します。

1. HTML構造の設計

ナビゲーション領域は<nav>要素で囲み、メニュー項目は<ul>/<li>/<a>で構成します。モバイル表示時にメニューの表示・非表示を切り替えるためのボタンを設置します。

<header>
  <div class="header-inner">
    <h1>サイトタイトル</h1>
    <button class="menu-toggle" aria-expanded="false" aria-controls="global-nav">
      <span class="menu-text">メニュー</span>
      <!-- ハンバーガーアイコンなどをSVGで追加 -->
    </button>
    <nav id="global-nav" class="global-nav" hidden>
      <ul>
        <li><a href="#section1">セクション1</a></li>
        <li><a href="#section2">セクション2</a></li>
        <li><a href="#section3">セクション3</a></li>
        <li><a href="#section4">セクション4</a></li>
      </ul>
    </nav>
  </div>
</header>

2. ARIA属性の活用

ボタンとメニューの関連性、およびメニューの開閉状態を支援技術に伝えます。

これらの属性はJavaScriptでメニューの開閉状態に合わせて動的に更新する必要があります。

3. JavaScriptによるメニュー開閉の実装

ボタンのクリックやキーボード操作に応じて、メニューの表示・非表示とARIA属性を切り替えるJavaScriptを記述します。

const menuToggle = document.querySelector('.menu-toggle');
const globalNav = document.getElementById('global-nav');

menuToggle.addEventListener('click', () => {
  const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
  menuToggle.setAttribute('aria-expanded', String(!isExpanded)); // 状態を反転して設定

  if (!isExpanded) {
    globalNav.removeAttribute('hidden'); // hidden属性を削除して表示
    // メニュー内の最初の要素にフォーカスを移動(推奨)
    const firstLink = globalNav.querySelector('a');
    if (firstLink) {
      firstLink.focus();
    }
  } else {
    globalNav.setAttribute('hidden', ''); // hidden属性を設定して非表示
    // メニューを閉じたらトグルボタンにフォーカスを戻す(推奨)
    menuToggle.focus();
  }
});

// Escキーでメニューを閉じる機能の追加
document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !globalNav.hasAttribute('hidden')) {
    menuToggle.setAttribute('aria-expanded', 'false');
    globalNav.setAttribute('hidden', '');
    menuToggle.focus(); // トグルボタンにフォーカスを戻す
  }
});

// メニューの外側をクリックまたはフォーカスが外れたら閉じる(オプションだが推奨)
// これは少し複雑になるため、ここでは基本的な例に留めます。
// 実際には、メニューとトグルボタンを含む特定の領域外かを判定する必要があります。
// document.addEventListener('click', (event) => {
//   if (!header.contains(event.target) && !globalNav.hasAttribute('hidden')) {
//     menuToggle.setAttribute('aria-expanded', 'false');
//     globalNav.setAttribute('hidden', '');
//     menuToggle.focus();
//   }
// });

// フォーカストラップに関する考慮も重要ですが、ここでは割愛します。
// 完全なアクセシブルなメニューでは、メニューが開いている間、フォーカスをメニュー内に閉じ込める(フォーカストラップ)実装が望ましいです。

4. CSSによるスタイリングとレスポンシブ対応

初期状態やメニュー開閉時のスタイルをCSSで定義し、メディアクエリを使ってレスポンシブに対応させます。

/* 初期状態: モバイル向け */
.menu-toggle {
  display: block; /* モバイルでは表示 */
  /* スタイリング */
}

.global-nav {
  /* 初期状態ではhidden属性で非表示 */
  position: absolute; /* もしくは fixed */
  top: 100%;
  left: 0;
  width: 100%;
  background-color: #fff; /* 例 */
  /* その他のスタイリング */
}

/* hidden属性が付いている場合は非表示 */
.global-nav[hidden] {
  display: none;
}

/* デスクトップ向けスタイル */
@media (min-width: 768px) { /* 例: ブレークポイント */
  .menu-toggle {
    display: none; /* デスクトップでは非表示 */
  }

  .global-nav {
    position: static; /* 通常配置に戻す */
    width: auto;
    /* hidden属性を上書きして常に表示 */
    display: block !important; /* !importantは本来避けるべきだが、hidden属性のdisplay: noneを確実に上書きするために使用することがある。より良い方法はJS側でhidden属性の制御をメディアクエリと連携させること。 */
    /* その他のスタイリング */
  }

  .global-nav[hidden] {
    /* デスクトップではhidden属性があっても表示されるように、display: block を再度指定 */
    display: block;
  }
}

より良いレスポンシブ対応のJavaScript制御

メディアクエリとJavaScriptを連携させ、画面幅に応じてメニューの表示制御方法を変える方が、CSSの!importantなどを使わずに済みます。

const menuToggle = document.querySelector('.menu-toggle');
const globalNav = document.getElementById('global-nav');
const breakpoint = 768; // CSSのブレークポイントと合わせる

function handleResize() {
  if (window.innerWidth >= breakpoint) {
    // デスクトップサイズの場合
    globalNav.removeAttribute('hidden'); // hidden属性を削除して常に表示
    menuToggle.setAttribute('aria-expanded', 'true'); // デスクトップでは常に開いているとみなす
    menuToggle.setAttribute('tabindex', '-1'); // デスクトップではボタンをフォーカス不可にする
  } else {
    // モバイルサイズの場合
    // 初期状態または閉じた状態に設定
    if (menuToggle.getAttribute('aria-expanded') !== 'true') {
       globalNav.setAttribute('hidden', '');
    }
    menuToggle.setAttribute('tabindex', '0'); // モバイルではボタンをフォーカス可能にする
  }
}

// 初期ロード時とリサイズ時に実行
window.addEventListener('resize', handleResize);
handleResize(); // ページロード時にも実行

// モバイルサイズでのボタンクリックイベント(上記のコードと組み合わせる)
menuToggle.addEventListener('click', () => {
  if (window.innerWidth < breakpoint) { // モバイルサイズのみで有効
    const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
    menuToggle.setAttribute('aria-expanded', String(!isExpanded));

    if (!isExpanded) {
      globalNav.removeAttribute('hidden');
      const firstLink = globalNav.querySelector('a');
      if (firstLink) {
        firstLink.focus();
      }
    } else {
      globalNav.setAttribute('hidden', '');
      menuToggle.focus();
    }
  }
});

// Escキーでメニューを閉じる(モバイルサイズのみ有効にするなど調整が必要)
document.addEventListener('keydown', (event) => {
  if (window.innerWidth < breakpoint) { // モバイルサイズのみで有効
     if (event.key === 'Escape' && !globalNav.hasAttribute('hidden')) {
      menuToggle.setAttribute('aria-expanded', 'false');
      globalNav.setAttribute('hidden', '');
      menuToggle.focus();
    }
  }
});

この方法では、JavaScriptが画面幅を判定し、デスクトップ幅ではメニューを常に表示し、モバイル幅でのみボタンによる開閉制御を行うように切り替えます。これにより、CSSでの複雑な上書きを避けられます。

実装時の注意点

テスト方法

実装したナビゲーションメニューがアクセシブルであるかを確認するために、以下の方法でテストを行います。

  1. キーボード操作:
    • Tabキーでページ内を移動し、ナビゲーションのトグルボタンにフォーカスが当たるか確認します。
    • EnterキーまたはSpaceキーでボタンを操作し、メニューが開閉することを確認します。
    • メニューが開いている状態でTabキーを押し、メニュー項目間を順に移動できるか確認します。メニュー内の最後の項目からTabキーを押した場合、フォーカスがメニューの外に移動するか、あるいはメニュー内の最初の項目に戻るか(フォーカストラップ実装時)を確認します。
    • Escキーでメニューが閉じるか確認します。
    • メニューが閉じた後、フォーカスがどこに戻るか(理想的にはトグルボタン)確認します。
  2. スクリーンリーダー:
    • VoiceOver (macOS/iOS)、NVDA (Windows)、JAWS (Windows) などのスクリーンリーダーを使用してページを操作します。
    • トグルボタンにフォーカスを当てた際に、ボタンの役割と状態(例: 「メニュー、閉じるボタン」、「展開されています」など)が正しく読み上げられるか確認します。
    • メニューが開閉する際に、状態の変化が読み上げられるか確認します。
    • メニュー内のリンクが正しく読み上げられ、ナビゲートできるか確認します。
    • 非表示状態のメニューがスクリーンリーダーで読み上げられないことを確認します。
  3. アクセシビリティ評価ツール:
    • Lighthouse、 tota11y、 axe DevTools などの自動評価ツールを使用して、基本的なアクセシビリティの問題(ARIA属性の誤りなど)がないかスキャンします。ただし、自動ツールはすべての問題を発見できるわけではないため、手動テストは不可欠です。
  4. 手動でのDOM構造確認:
    • ブラウザの開発者ツールで、メニューの開閉時にhidden属性やaria-expanded属性が正しく切り替わっているかを確認します。

まとめ

アクセシブルなナビゲーションメニューの実装は、すべてのユーザーがウェブサイトを円滑に利用するために非常に重要です。特にレスポンシブ対応のメニューでは、単に見た目を切り替えるだけでなく、キーボード操作やスクリーンリーダーのユーザーがメニューの状態を理解し、操作できるように配慮する必要があります。

セマンティックなHTML構造、aria-expandedaria-controlsといったARIA属性の適切な利用、そしてJavaScriptによるキーボード操作とフォーカス管理の実装は、ナビゲーションのアクセシビリティを高めるための鍵となります。

この記事で解説した手順と注意点を参考に、あなたのウェブサイトのナビゲーションをより多くの人々にとって使いやすいものにしていただければ幸いです。定期的なテストを通じて、実装の正確性を確認することも忘れないでください。