アクセシブルなドロップダウンメニューの実装:キーボード操作とARIA活用
はじめに
ドロップダウンメニューは、限られたスペースに多くのナビゲーションリンクやオプションを配置するためにウェブサイトで頻繁に使用されるUIコンポーネントです。しかし、適切に実装されないと、キーボードユーザーやスクリーンリーダーユーザーなど、多くの利用者にとって操作が困難になる可能性があります。
この記事では、HTML、CSS、JavaScript、そしてARIA属性を組み合わせて、すべての利用者が容易に操作できるアクセシブルなドロップダウンメニューを実装する具体的な方法を解説します。
ドロップダウンメニューのアクセシビリティ課題
一般的なドロップダウンメニューの実装では、以下のようなアクセシビリティに関する課題が見られます。
- マウス依存: マウスオーバーやクリックのみでメニューが開閉し、キーボード操作に対応していない。
- フォーカス管理の欠如: メニューが開閉した際に、適切にフォーカスが管理されず、ユーザーが現在どの要素にいるのか分からなくなる。
- スクリーンリーダーへの情報不足: メニューの状態(開いているか閉じているか)や、ポップアップであることをスクリーンリーダーに適切に伝達できていない。
- キーボードトラップ: メニュー内でフォーカスが閉じ込められ、Escキーなどでメニューを閉じたり、メニュー外に移動したりできない。
これらの課題により、キーボードのみで操作するユーザー、視覚障害がありスクリーンリーダーを利用するユーザー、運動機能障害を持つユーザーなどが、ウェブサイトのナビゲーションを効果的に利用できなくなります。
なぜドロップダウンメニューのアクセシビリティが重要なのか
Web Content Accessibility Guidelines (WCAG) には、操作可能(Operable)に関する複数の達成基準があります。特にキーボード操作に関する基準は、ドロップダウンメニューのようなインタラクティブなコンポーネントにおいて重要です。
- 2.1.1 キーボード: コンテンツのすべての機能をキーボードから利用できるようにすること。
- 2.4.3 フォーカス順序: タブキーなどによるフォーカス移動の順序が論理的であること。
- 2.4.7 フォーカス可視: キーボードフォーカスが当たっている要素が視覚的に明確にわかるようにすること。
アクセシブルなドロップダウンメニューを実装することは、これらの達成基準を満たし、より多くのユーザーがサイトを快適に利用できるようにするために不可欠です。
アクセシブルなドロップダウンメニューの基本構造(HTML)
セマンティックなHTML構造は、アクセシビリティの基盤となります。ナビゲーションメニューにはnav
要素、リスト構造にはul
とli
要素を使用することが推奨されます。
以下に、基本的なHTML構造の例を示します。
<nav aria-label="メインナビゲーション">
<ul>
<li><a href="/">ホーム</a></li>
<li class="has-submenu">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="submenu-about"
>
会社情報
</button>
<ul id="submenu-about" class="submenu">
<li><a href="/about/philosophy">経営理念</a></li>
<li><a href="/about/history">沿革</a></li>
<li><a href="/about/access">アクセス</a></li>
</ul>
</li>
<li><a href="/products">製品情報</a></li>
<li class="has-submenu">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="submenu-contact"
>
お問い合わせ
</button>
<ul id="submenu-contact" class="submenu">
<li><a href="/contact/form">フォームで問い合わせ</a></li>
<li><a href="/contact/faq">よくある質問</a></li>
</ul>
</li>
</ul>
</nav>
この構造では、以下の点を考慮しています。
nav
要素でナビゲーションセクションをマークアップし、aria-label
で補足的な説明を付けています。- サブメニューを持つ項目には、トリガーとなる要素として
button
を使用しています。<a>
要素をトリガーとして使うこともありますが、button
はクリック可能であり、キーボード操作やロールのセマンティクスが明確なため、多くの場合はこちらが適しています。<a>
を使う場合は、適切なrole
やtabindex="0"
などの考慮が必要です。 - サブメニューのリストは、トリジナリストの子要素として配置しています。
aria-haspopup="true"
: このボタンがポップアップ要素(ここではサブメニュー)をトリガーすることを示します。スクリーンリーダーに、このボタンを押すとリストが開く可能性があることを伝えます。aria-expanded="false"
: ポップアップ要素(サブメニュー)が現在閉じている状態であることを示します。メニューが開いたらJavaScriptで"true"
に変更します。aria-controls="[サブメニューのID]"
: このボタンが制御する要素(サブメニューのul
)のIDを指定します。これにより、ボタンとサブメニューの関係性をスクリーンリーダーに伝えることができます。
キーボード操作への対応
アクセシブルなドロップダウンメニューでは、マウスを使わずにキーボードだけで開閉、移動、選択ができる必要があります。
1. メニューを開閉するキーボード操作
- SpaceキーまたはEnterキー: サブメニューのトリガー(
button
要素)にフォーカスがあるときにこれらのキーを押すと、サブメニューが開閉するようにします。button
要素はデフォルトでこれらの操作に対応しているため、特別なJavaScriptは不要な場合がありますが、確実な制御のためにイベントリスナーを設定することも可能です。
2. メニュー項目を移動するキーボード操作
- 下矢印キー (↓): サブメニューのトリガーにフォーカスがあるときに押すと、サブメニューを開き、最初の項目にフォーカスを移動させます。サブメニュー内の項目にフォーカスがあるときに押すと、次の項目にフォーカスを移動させます。
- 上矢印キー (↑): サブメニュー内の項目にフォーカスがあるときに押すと、前の項目にフォーカスを移動させます。
- 左右矢印キー (← →): 水平方向のナビゲーションがある場合(例: グローバルナビゲーションで複数のドロップダウンがある場合)、現在のドロップダウンを閉じ、隣のドロップダウンのトリガーにフォーカスを移動させるために使用できます。
3. メニューを閉じるキーボード操作
- Escキー: 開いているサブメニュー内のどこかにフォーカスがあるときにEscキーを押すと、サブメニューを閉じ、フォーカスを元のトリガー(
button
要素)に戻します。 - Tabキー: サブメニュー内の最後の項目にフォーカスがあるときにTabキーを押すと、サブメニューを閉じ、次のフォーカス可能な要素(通常はナビゲーションの次のトップレベル項目、またはその後のページの要素)にフォーカスを移動させます。サブメニュー外のフォーカス可能な要素にTabで移動した際もメニューが閉じることが望ましいです。
ARIA属性の活用
前述のHTML構造で紹介したARIA属性は、ドロップダウンメニューの状態や役割を補助技術に正確に伝えるために不可欠です。
aria-haspopup="true"
: トリガー要素がポップアップ(サブメニュー)を持つことを示します。aria-expanded="false"
/"true"
: トリガー要素が制御するパネルやグループ(サブメニュー)が現在閉じているか開いているかを示します。JavaScriptで状態に合わせて動的に変更します。aria-controls="[サブメニューのID]"
: トリガー要素が制御する要素(サブメニュー)のIDを指定し、関連付けを行います。role="menu"
とrole="menuitem"
: よりリッチなアプリケーションメニューの場合に検討します。ただし、これはネイティブのメニューバーのような振る舞いを模倣するためのものであり、単純なナビゲーションドロップダウンには適さない場合もあります。一般的なウェブサイトのナビゲーションであれば、<ul>
と<li>
のセマンティクスを維持しつつ、aria-haspopup
やaria-expanded
を使用する方がシンプルで実装しやすいでしょう。本記事では後者のアプローチを中心に解説します。
JavaScriptによる実装(開閉とキーボード操作の制御)
JavaScriptを使用して、ドロップダウンメニューの開閉ロジックとキーボードイベントのハンドリングを実装します。
document.addEventListener('DOMContentLoaded', () => {
const nav = document.querySelector('nav[aria-label="メインナビゲーション"]');
if (!nav) return;
const subMenuTriggers = nav.querySelectorAll('.has-submenu > button');
subMenuTriggers.forEach(trigger => {
const subMenuId = trigger.getAttribute('aria-controls');
const subMenu = document.getElementById(subMenuId);
if (!subMenu) return;
// 初期状態設定 (CSSでデフォルト非表示にしている前提)
trigger.setAttribute('aria-expanded', 'false');
subMenu.style.display = 'none'; // スタイルでの制御例
// メニュー開閉関数
const toggleSubMenu = (isOpen) => {
trigger.setAttribute('aria-expanded', isOpen);
subMenu.style.display = isOpen ? 'block' : 'none'; // スタイルでの制御例
// メニューを開いた場合、最初の項目にフォーカスを移動
if (isOpen) {
const firstItem = subMenu.querySelector('a, button');
if (firstItem) {
firstItem.focus();
}
} else {
// メニューを閉じた場合、トリガーにフォーカスを戻す (Escキー以外の場合に検討)
// ここではEscキーのハンドリングでフォーカス制御を行うため省略
}
};
// クリックイベントで開閉
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
toggleSubMenu(!isExpanded);
// 他の開いているメニューを閉じる処理を追加することも多い
subMenuTriggers.forEach(otherTrigger => {
if (otherTrigger !== trigger && otherTrigger.getAttribute('aria-expanded') === 'true') {
const otherSubMenuId = otherTrigger.getAttribute('aria-controls');
const otherSubMenu = document.getElementById(otherSubMenuId);
if(otherSubMenu) {
otherTrigger.setAttribute('aria-expanded', 'false');
otherSubMenu.style.display = 'none';
}
}
});
});
// キーボードイベントのハンドリング
trigger.addEventListener('keydown', (event) => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
switch (event.key) {
case ' ': // Space
case 'Enter':
event.preventDefault(); // デフォルトのボタン動作を抑止
toggleSubMenu(!isExpanded);
break;
case 'ArrowDown': // 下矢印キー
event.preventDefault();
if (!isExpanded) {
toggleSubMenu(true); // メニューが開いていなければ開く
} else {
// 既に開いている場合は、最初の項目にフォーカスはtoggleSubMenu内で実施
}
break;
// 他のキーボード操作 (例: 左右矢印で隣のメニューへ移動) もここに追加
}
});
// サブメニュー内のキーボードイベントハンドリング (主に矢印キー、Escキー)
subMenu.addEventListener('keydown', (event) => {
const focusableItems = Array.from(subMenu.querySelectorAll('a, button')).filter(item => item.offsetParent !== null); // 表示されているフォーカス可能な要素を取得
const focusedIndex = focusableItems.indexOf(document.activeElement);
switch (event.key) {
case 'ArrowDown': // 下矢印キー
event.preventDefault();
if (focusedIndex < focusableItems.length - 1) {
focusableItems[focusedIndex + 1].focus(); // 次の項目へ
} else {
// 最後の項目の場合、先頭に戻るか、何もしないか選択
// focusableItems[0].focus(); // 先頭に戻る例
}
break;
case 'ArrowUp': // 上矢印キー
event.preventDefault();
if (focusedIndex > 0) {
focusableItems[focusedIndex - 1].focus(); // 前の項目へ
} else {
// 最初の項目の場合、最後の項目に戻るか、何もしないか選択
// focusableItems[focusableItems.length - 1].focus(); // 最後に戻る例
}
break;
case 'Escape': // Escキー
event.preventDefault();
toggleSubMenu(false); // メニューを閉じる
trigger.focus(); // トリガーにフォーカスを戻す
break;
case 'Tab': // Tabキー
// Tabによる移動はデフォルトのブラウザ動作に任せる
// shift+Tabの場合は前の要素へ移動
if (event.shiftKey && focusedIndex === 0) {
// 最初の項目でShift+Tabの場合、メニューを閉じてトリガーにフォーカスを戻す
toggleSubMenu(false);
trigger.focus();
event.preventDefault(); // デフォルトのShift+Tab動作を抑止
} else if (!event.shiftKey && focusedIndex === focusableItems.length - 1) {
// 最後の項目でTabの場合、メニューを閉じる
toggleSubMenu(false);
// 次の要素へのフォーカス移動はブラウザに任せる
}
// それ以外の場合はデフォルトのTab/Shift+Tab動作
break;
}
});
// サブメニュー外をクリックしたら閉じる処理
document.addEventListener('click', (event) => {
const isClickInside = nav.contains(event.target);
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (!isClickInside && isExpanded) {
toggleSubMenu(false);
}
});
// サブメニュー外にフォーカスが移動したら閉じる処理 (Tab/Shift+Tabでの移動を考慮)
document.addEventListener('focusin', (event) => {
const isFocusInside = nav.contains(event.target);
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (!isFocusInside && isExpanded) {
toggleSubMenu(false);
}
});
}); // end forEach subMenuTriggers
});
このJavaScriptコードは、以下の機能を提供します。
- トリガーボタンのクリックによるメニューの開閉。
aria-expanded
属性の動的な更新。- トリガーボタンにフォーカスがある状態でのSpace/Enterキーによる開閉。
- トリガーボタンにフォーカスがある状態での下矢印キーによるメニューの開放と、最初の項目へのフォーカス移動。
- サブメニュー内の項目にフォーカスがある状態での上/下矢印キーによる項目間の移動。
- サブメニュー内のどこかにフォーカスがある状態でのEscキーによるメニューの閉鎖と、トリガーへのフォーカス戻し。
- サブメニュー内の項目にフォーカスがある状態でのTab/Shift+Tabキーによる移動と、必要に応じたメニューの閉鎖。
- メニュー外をクリックまたはフォーカス移動した場合のメニューの閉鎖。
実装時の注意点
- CSSでの非表示: サブメニューは初期状態で非表示にする必要があります。
display: none;
を使用すると、要素がアクセシビリティツリーから削除されるため、スクリーンリーダーが内容を読み上げなくなります。メニューが開いているときだけ表示するためにJavaScriptでdisplay
プロパティを切り替えるのは一般的な手法ですが、アニメーションを伴う場合や、表示/非表示をCSSだけで制御したい場合は、visibility: hidden;
とopacity: 0;
を使用し、開いているときにvisibility: visible;
とopacity: 1;
に変更する方法もあります。この場合、要素はアクセシビリティツリーに残りますが、CSSで非表示の状態であることをスクリーンリーダーが解釈するかどうかは、ブラウザやスクリーンリーダーの実装に依存する場合があります。状況に応じて適切な方法を選択してください。 - フォーカスリング: キーボードフォーカスが現在どの要素にあるのか視覚的に明確に示すために、フォーカスリングは非常に重要です。
outline
プロパティなどを適切に設定し、outline: none;
で非表示にしないように注意してください。 - マウスオーバーによる開閉: マウスオーバーでドロップダウンを開く場合、キーボードユーザーのために、キーボード操作でも開閉できる代替手段(上記のボタンによる開閉など)を必ず提供してください。また、マウスオーバーで開く動作は、手の震えなどがあるユーザーには意図しないメニューの開閉を引き起こす可能性があるため、推奨されない場合が多いです。クリックによる開閉がより安定した操作を提供します。
- タッチデバイス: タッチデバイスではマウスオーバーが存在しないため、タッチ操作(タップ)による開閉が必須です。また、サブメニューが開いたときに、その中のリンクにアクセスできるよう、親要素のタップでリンク先に遷移しないような実装が必要です。
- ネストされたメニュー: ドロップダウンメニューの中にさらにドロップダウンメニューがある場合(ネストされたメニュー)、キーボードナビゲーション、フォーカス管理、ARIA属性の使い方がより複雑になります。矢印キーでの移動(例: 右矢印で子メニューを開く、左矢印で親メニューに戻る)や、ARIA属性の適切な継承・関連付けが必要になります。
実装したアクセシビリティのテスト方法
実装したドロップダウンメニューがアクセシブルであるかを検証するには、以下の方法を組み合わせて行うことが重要です。
- キーボード操作での確認:
- Tabキー、Shift+Tabキーでページの全ての要素を順に移動し、ドロップダウンのトリガーに正しくフォーカスが当たるか確認します。フォーカス順序が論理的かどうかも確認してください。
- トリガーにフォーカスが当たった状態で、Spaceキー、Enterキー、下矢印キーを押してメニューが開くか確認します。
- メニューが開いた状態で、上矢印キー、下矢印キーでメニュー項目間を移動できるか確認します。ループバック(最初から最後、最後から最初への移動)の挙動も確認します。
- メニューが開いた状態で、Escキーを押してメニューが閉じ、フォーカスがトリガーに戻るか確認します。
- メニューが開いた状態で、メニューの最後の項目にフォーカスを当て、Tabキーを押してメニューが閉じ、次のフォーカス可能な要素に移動するか確認します。
- メニューが開いた状態で、メニューの最初の項目にフォーカスを当て、Shift+Tabキーを押してメニューが閉じ、トリガーにフォーカスが戻るか確認します。
- スクリーンリーダーでの確認:
- 主要なスクリーンリーダー(PCではNVDA, JAWS, VoiceOver、モバイルではVoiceOver, TalkBackなど)を使用して、ドロップダウンメニューを操作してみてください。
- トリガー要素にフォーカスが当たったときに、その役割(ボタンなど)や状態(展開可能であること、現在閉じていることなど -
aria-haspopup
,aria-expanded
の情報)が正しく読み上げられるか確認します。 - メニューを開いた後、メニュー項目がリストとして認識され、各項目に正しく移動できるか確認します。
- メニュー項目を選択したり、Escキーで閉じたりした際に、期待通りの動作と読み上げが行われるか確認します。
- アクセシビリティ評価ツールの活用:
- Lighthouse, axe DevTools, Web Accessibility Evaluation Tool (WAVE) などの自動評価ツールを使用し、ARIA属性の構文エラーや一般的なアクセシビリティ違反がないかチェックします。ただし、これらのツールは自動で検出できる問題に限定されるため、必ず手動でのキーボード・スクリーンリーダーテストと併用してください。
まとめ
アクセシブルなドロップダウンメニューの実装は、HTMLのセマンティクス、CSSでの状態管理、JavaScriptでのインタラクション制御、そしてARIA属性によるセマンティクスの補強を組み合わせることで実現できます。特にキーボード操作とフォーカス管理、ARIA属性による状態伝達は、多くのユーザーがウェブサイトのナビゲーションを円滑に利用するために不可欠です。
この記事で紹介したコード例や手順を参考に、ご自身のプロジェクトでアクセシブルなドロップダウンメニューの実装に取り組んでみてください。実装後は、様々な方法で入念なテストを行い、すべての利用者が快適に利用できることを確認することが重要です。アクセシビリティ対応は一度行えば終わりではなく、継続的な改善が求められる取り組みです。