ナビゲーション
メニューボタン
解説ありMenu Button
押すとメニューを開くボタン。aria-haspopup と aria-expanded で関係を伝えます。
メニューボタン(Menu Button)とは?
メニューボタンは、押すと操作の一覧(メニュー)がポップアップするボタンです。 「⋯」や「操作 ▾」のようなボタンを押すと「複製・名前を変更・削除」などが出てくる、あれです。 アプリ的なコマンドの集まりを、場所を取らずに提供できます。
補足
ここでいう「メニュー」はアプリのコマンド用です。サイト内を移動するための ナビゲーションには通常 role="menu" を使わず、<nav> + リンクのリストにします(詳しくはメニューバーのページを参照)。
なぜアクセシビリティが大事なの?
- キーボードだけで操作する人。
:hover依存のドロップダウンは マウスがないと永遠に開けません。Enter や ↓ で開き、↑↓ で項目を選び、Esc で閉じられる必要があります。 - スクリーンリーダーを使う人。
aria-haspopup="menu"があると 「メニューを開くボタン」だと分かり、aria-expandedで開閉状態が伝わります。 開いたらフォーカスをメニュー内へ移すこと、閉じたらトリガーへ戻すことが大切です。
ライブデモ(推奨実装)
下のメニューボタンは APG に沿った実装です。マウスを使わず操作してみてください。
試してみよう:Tab でボタンへ → Enter / Space / ↓ で開く(先頭にフォーカス)→ ↑ ↓ で項目移動、Home / End で端へ → Enter で実行 → Esc で閉じてボタンに戻る。
ポイント
開閉時のフォーカス移動がポイントです。開いたら最初の項目へ、Esc で閉じたらボタンへフォーカスを戻すことで、 キーボード利用者が「今どこにいるか」を見失いません。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| Enter / Space / ↓(ボタン上) | メニューを開き、最初の項目へフォーカス | 必須 |
| ↑(ボタン上) | メニューを開き、最後の項目へフォーカス | 任意 |
| ↑ / ↓(メニュー内) | 前 / 次の項目へフォーカス移動 | 必須 |
| Home / End | 最初 / 最後の項目へ移動 | 任意(推奨) |
| Enter / Space(項目上) | 項目を実行し、メニューを閉じてボタンへ戻る | 必須 |
| Esc | メニューを閉じてボタンへフォーカスを戻す | 必須 |
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| トリガー | <button> + aria-haspopup="menu" | 押すとメニューが開くボタンであることを伝える。 |
| トリガー | aria-expanded="true | false" | メニューが開いているか。開閉に合わせ必ず更新する。 |
| トリガー | aria-controls="メニューのid" | どのメニューを開くボタンかを示す。 |
| メニューの箱 | role="menu" + aria-labelledby | メニューであることと、トリガーを名前として持つことを伝える。 |
| 各項目 | role="menuitem" + tabindex="-1" | メニュー項目であることを伝える。フォーカスは JS で管理(roving)。 |
| 閉じたメニュー | hidden | 閉じている間は読み上げ/操作の対象外にする。 |
実装:推奨パターン(Good)
良い例 / 推奨
トリガーは <button aria-haspopup="menu" aria-expanded>、メニューはrole="menu" + role="menuitem"。開閉時のフォーカス移動とEsc 復帰を JS で実装します。
マークアップ:
<div class="menu">
<button type="button"
id="menu-button-trigger"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menu-button-list">
操作 ▾
</button>
<ul id="menu-button-list"
role="menu"
aria-labelledby="menu-button-trigger"
hidden>
<li role="menuitem" tabindex="-1">複製</li>
<li role="menuitem" tabindex="-1">名前を変更</li>
<li role="menuitem" tabindex="-1">削除</li>
</ul>
</div>開閉・フォーカス管理のスクリプト:
const trigger = document.getElementById('menu-button-trigger');
const menu = document.getElementById('menu-button-list');
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
function open(focusIndex) {
menu.hidden = false;
trigger.setAttribute('aria-expanded', 'true');
items[focusIndex].focus(); // 開いたらメニュー内へフォーカスを移す
}
function close(returnFocus = true) {
menu.hidden = true;
trigger.setAttribute('aria-expanded', 'false');
if (returnFocus) trigger.focus(); // Esc 等ではトリガーへ戻す
}
trigger.addEventListener('click', () =>
menu.hidden ? open(0) : close());
trigger.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); open(0);
} else if (e.key === 'ArrowUp') {
e.preventDefault(); open(items.length - 1);
}
});
items.forEach((item, i) => {
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length].focus(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); }
else if (e.key === 'Home') { e.preventDefault(); items[0].focus(); }
else if (e.key === 'End') { e.preventDefault(); items[items.length - 1].focus(); }
else if (e.key === 'Escape') { close(); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); run(item); }
});
item.addEventListener('click', () => run(item));
});
function run(item) { /* 実行処理 */ close(); }
// メニューの外をクリックしたら閉じる
document.addEventListener('click', (e) => {
if (!menu.hidden && !e.target.closest('.menu')) close(false);
});アンチパターン(Bad)
下は :hover だけで開くドロップダウンです。マウスを乗せれば開きますが、キーボードでは開くことすらできません。
- 複製
- 名前を変更
- 削除
試してみよう:マウスを乗せると開きますが、Tab でトリガーにフォーカスできず、↓ でも Esc でも操作できません。マウスを外すと即閉じます。
<!-- ❌ アンチパターン:hover だけで開くドロップダウン -->
<div class="dropdown">
<div class="trigger">操作 ▾</div>
<!-- マウスを乗せている間だけ CSS で表示。aria もキーボードもない -->
<ul class="menu-list">
<li onclick="run('copy')">複製</li>
<li onclick="run('rename')">名前を変更</li>
<li onclick="run('delete')">削除</li>
</ul>
</div>
<style>
.menu-list { display: none; }
.dropdown:hover .menu-list { display: block; } /* hover 依存 */
</style>悪い例 / 避ける
この実装の問題点:
- キーボードで開けない —
divトリガーはフォーカス不可。↓/Enter も効かない。 - hover 依存 — マウスが使えない人・タッチ環境で破綻し、マウスを外すと閉じてしまう。
- 役割と状態が伝わらない —
aria-haspopup/aria-expanded/role="menu"がない。 - Esc で閉じられない・フォーカス管理がない — どこにいるか見失う。
実装チェックリスト
- トリガーは
<button>でaria-haspopup="menu"+aria-expanded+aria-controls - メニューは
role="menu"、項目はrole="menuitem"+tabindex="-1" - クリック / Enter / Space / ↓ で開き、最初の項目へフォーカスする
- メニュー内は ↑↓ で移動、Home/End で端へ
- Enter/Space で項目を実行し、閉じてトリガーへ戻る
- Esc で閉じてトリガーへフォーカスを戻す
- メニューの外をクリックしたら閉じる
aria-expandedが開閉と常に一致し、フォーカスが見える