アクセシブルなナビゲーションメニューの実装:キーボード操作とARIA属性、レスポンシブ対応
はじめに
ウェブサイトのナビゲーションメニューは、ユーザーがサイト内を移動するために不可欠な要素です。特にモバイルファーストやレスポンシブデザインが主流となった現在、いわゆる「ハンバーガーメニュー」に代表されるような、表示・非表示を切り替えるタイプのナビゲーションが多く使用されています。
しかし、これらのメニューがマウス操作のみに依存している場合、キーボードユーザーやスクリーンリーダーユーザーはサイト内のコンテンツに効果的にアクセスできなくなる可能性があります。Webアクセシビリティの観点から、すべてのユーザーが容易にナビゲーションを利用できるよう、適切な実装が求められます。
この記事では、ナビゲーションメニュー、特にレスポンシブ対応で表示・非表示を切り替えるタイプのメニューをアクセシブルにするための具体的な実装方法について、キーボード操作、ARIA属性、そしてJavaScriptによる制御に焦点を当てて解説します。
なぜアクセシブルなナビゲーションが必要か
アクセシブルなナビゲーションメニューは、多様なユーザーにとってサイトの使いやすさを向上させます。
- キーボードユーザー: マウスを使用せず、キーボード(Tabキー、Enterキー、Spaceキーなど)のみで操作するユーザーは、ナビゲーションメニューを開いたり、メニュー項目間を移動したり、メニューを閉じたりできる必要があります。
- スクリーンリーダーユーザー: 視覚情報に頼らず、スクリーンリーダーを使用してコンテンツを読み上げるユーザーは、メニューの状態(開いているか閉じているか)、メニュー内の構造、そして各リンクの目的を正確に理解できる必要があります。
- モバイルユーザー: タッチ操作だけでなく、音声入力やスイッチコントロールなどの支援技術を利用するモバイルユーザーも、ナビゲーションを問題なく利用できる必要があります。
適切なHTML構造、ARIA属性、そしてJavaScriptによる制御を組み合わせることで、これらのユーザーもスムーズにサイトを探索できるようになります。
実装の基本方針
アクセシブルなナビゲーションメニューを実装するための基本的な要素は以下の通りです。
- セマンティックHTML: ナビゲーション領域には
<nav>
要素を使用し、メニュー項目はリスト(<ul>
や<ol>
)とリストアイテム(<li>
)、リンク(<a>
)で構成します。メニューの開閉を制御する要素には<button>
を使用します。 - キーボード操作: Tabキーによる要素間の移動、Enter/Spaceキーによるボタンの操作、Escキーによるメニューの閉鎖など、キーボードでのすべての操作を可能にします。
- ARIA属性: 要素の状態(開閉など)や関連性(どのボタンがどのメニューを制御するか)を支援技術に伝えるために、ARIA属性(
aria-expanded
,aria-controls
,aria-haspopup
など)を適切に使用します。 - フォーカス管理: メニューが開閉した際に、ユーザーのフォーカスが適切な位置に移動するように制御します。例えば、メニューが開いたらメニュー内の最初の項目にフォーカスを移す、メニューを閉じたらトグルボタンに戻す、といった挙動です。
- 表示・非表示の制御: CSSの
display: none
やvisibility: 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>
<nav>
要素でナビゲーション領域であることを明示します。button
要素は、メニューを開閉するためのインタラクティブな要素であるため適切です。div
やspan
にクリックイベントを設定するよりもセマンティックで、デフォルトでキーボード操作可能です。- ボタンに表示するテキスト(ここでは「メニュー」)は、アイコンだけでなく提供することが推奨されます。CSSで視覚的に非表示にすることも可能ですが、要素自体にテキストを含めるのが最も確実です。
- メニュー本体には
id
属性を付与し、後述するaria-controls
で参照できるようにします。 - 初期状態ではメニューを非表示にするため、HTMLの
hidden
属性を使用します。これにより、メニューが非表示の際に支援技術から認識されなくなります。CSSでdisplay: none;
を指定するのと同じ効果がありますが、HTMLで状態を表現できるため推奨される場合があります。JavaScriptでhidden
属性を付け外しします。
2. ARIA属性の活用
ボタンとメニューの関連性、およびメニューの開閉状態を支援技術に伝えます。
aria-expanded
: ボタンが制御する要素(メニュー)が開いているか閉じているかを示します。メニューが閉じているときはfalse
、開いているときはtrue
を設定します。初期状態はfalse
です。aria-controls
: ボタンが制御する要素のid
を指定します。これにより、ボタンとメニューの関連性が明確になります。上記の例では、メニュー本体のid="global-nav"
を指定しています。aria-haspopup
(オプション): この属性は、要素がポップアップ(メニュー、サブメニュー、ダイアログなど)をトリガーすることを示します。メニューの場合はtrue
を指定することが多いですが、この属性は主にメニュー項目がサブメニューを持つ場合に利用されるため、メインのトグルボタンでは必須ではありませんが、追加することでメニューが開くことをより明確に伝えられます。この例では省略しています。
これらの属性は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();
// }
// });
// フォーカストラップに関する考慮も重要ですが、ここでは割愛します。
// 完全なアクセシブルなメニューでは、メニューが開いている間、フォーカスをメニュー内に閉じ込める(フォーカストラップ)実装が望ましいです。
- ボタンがクリックされるたびに、
aria-expanded
属性の値をトグル(true
とfalse
を切り替え)します。 hidden
属性を付け外しすることで、メニューの表示・非表示を制御します。- メニューが開いた際にメニュー内の最初のリンクにフォーカスを移すことで、キーボードユーザーがすぐにメニュー項目にアクセスできるようになります。
- メニューが閉じた際にトグルボタンにフォーカスを戻すことで、ユーザーはどこからメニューを開いたかを失わず、閉じられたことを明確に認識できます。
- Escキーでメニューを閉じられるように、
keydown
イベントリスナーを追加します。これは一般的なUIのアクセシビリティパターンです。Escキーで閉じた際も、トグルボタンにフォーカスを戻します。
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;
}
}
- モバイル向けの初期状態では、トグルボタンを表示し、メニュー本体は
hidden
属性によって非表示にします。 - JavaScriptによって
hidden
属性が削除されると、メニューが表示されます。CSSで表示時の位置やスタイルを調整します。 - メディアクエリを使用して、デスクトップ幅になったらトグルボタンを非表示にし、メニュー本体は常に表示されるようにスタイルを切り替えます。
- デスクトップ表示時にメニューを常に表示するためには、JavaScript側でデスクトップ幅では
hidden
属性を操作しないように制御するか、CSSのメディアクエリ内でdisplay: block;
などで[hidden]
セレクタのスタイルを上書きする必要があります。上記のCSS例では後者の方法を示していますが、JavaScriptで制御を分ける方がより柔軟です。
より良いレスポンシブ対応の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での複雑な上書きを避けられます。
実装時の注意点
- CSS
display: none
とvisibility: hidden
: これらのCSSプロパティは、要素を視覚的に非表示にするだけでなく、アクセシビリティツリーからも除外します。アクセシブルな非表示方法として適切です。一方で、opacity: 0
や画面外への配置などは、視覚的には見えなくても支援技術からは認識されてしまう可能性があるため、メニューの非表示には使用しないでください。hidden
属性の使用が推奨されます。 - フォーカストラップ: 完全なアクセシブルなモーダルやナビゲーションメニューでは、メニューが開いている間、フォーカスをメニュー内の要素のみに限定する「フォーカストラップ」の実装が望ましいです。これにより、ユーザーがTabキーを押し続けた際にメニューの外に出てしまうことを防ぎます。実装には、フォーカス可能な要素を特定し、Tabキーが押された際に最初または最後の要素から逆方向に移動する処理を加える必要があります。
- アニメーション: メニューの開閉にアニメーションを使用する場合、過度なアニメーションは一部のユーザーにとって不快となることがあります。
prefers-reduced-motion
メディアクエリを使用して、ユーザーがアニメーションを好まない設定にしている場合は、アニメーションを無効化または控えめにする配慮が望ましいです。 - メニュー内のサブメニュー: メニュー項目自体がドロップダウンやメガメニューなどのサブメニューを持つ場合、それらのサブメニューも同様にキーボード操作とARIA属性(
aria-haspopup
,aria-expanded
,aria-controls
など)に対応させる必要があります。
テスト方法
実装したナビゲーションメニューがアクセシブルであるかを確認するために、以下の方法でテストを行います。
- キーボード操作:
- Tabキーでページ内を移動し、ナビゲーションのトグルボタンにフォーカスが当たるか確認します。
- EnterキーまたはSpaceキーでボタンを操作し、メニューが開閉することを確認します。
- メニューが開いている状態でTabキーを押し、メニュー項目間を順に移動できるか確認します。メニュー内の最後の項目からTabキーを押した場合、フォーカスがメニューの外に移動するか、あるいはメニュー内の最初の項目に戻るか(フォーカストラップ実装時)を確認します。
- Escキーでメニューが閉じるか確認します。
- メニューが閉じた後、フォーカスがどこに戻るか(理想的にはトグルボタン)確認します。
- スクリーンリーダー:
- VoiceOver (macOS/iOS)、NVDA (Windows)、JAWS (Windows) などのスクリーンリーダーを使用してページを操作します。
- トグルボタンにフォーカスを当てた際に、ボタンの役割と状態(例: 「メニュー、閉じるボタン」、「展開されています」など)が正しく読み上げられるか確認します。
- メニューが開閉する際に、状態の変化が読み上げられるか確認します。
- メニュー内のリンクが正しく読み上げられ、ナビゲートできるか確認します。
- 非表示状態のメニューがスクリーンリーダーで読み上げられないことを確認します。
- アクセシビリティ評価ツール:
- Lighthouse、 tota11y、 axe DevTools などの自動評価ツールを使用して、基本的なアクセシビリティの問題(ARIA属性の誤りなど)がないかスキャンします。ただし、自動ツールはすべての問題を発見できるわけではないため、手動テストは不可欠です。
- 手動でのDOM構造確認:
- ブラウザの開発者ツールで、メニューの開閉時に
hidden
属性やaria-expanded
属性が正しく切り替わっているかを確認します。
- ブラウザの開発者ツールで、メニューの開閉時に
まとめ
アクセシブルなナビゲーションメニューの実装は、すべてのユーザーがウェブサイトを円滑に利用するために非常に重要です。特にレスポンシブ対応のメニューでは、単に見た目を切り替えるだけでなく、キーボード操作やスクリーンリーダーのユーザーがメニューの状態を理解し、操作できるように配慮する必要があります。
セマンティックなHTML構造、aria-expanded
やaria-controls
といったARIA属性の適切な利用、そしてJavaScriptによるキーボード操作とフォーカス管理の実装は、ナビゲーションのアクセシビリティを高めるための鍵となります。
この記事で解説した手順と注意点を参考に、あなたのウェブサイトのナビゲーションをより多くの人々にとって使いやすいものにしていただければ幸いです。定期的なテストを通じて、実装の正確性を確認することも忘れないでください。