# ページではなくサブメニューが開いてしまうドロップダウンの親メニュー

Source: https://easeweb.dev/learn/menu-link-toggle
Topics: メニューとナビゲーション > サブメニューを持つメニュー項目も、リンクとしてたどれる (WCAG 2.1.1, 4.1.2)
The fix: 上段のリンクはそのまま残し、横にサブメニューを開いて開閉状態を伝える別のbuttonを置きます。または、項目自体をそのbuttonにし、項目のページは一覧の先頭のリンクに移します。どちらの方法でも、スクリプトでリンクを取り消すことはしません。
Test with: keyboard, screen reader, touch, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation-hybrid/ https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/ https://www.w3.org/WAI/tutorials/menus/flyout/ https://webaim.org/discussion/mail_message?id=29255

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

家具店のヘッダーに「ホーム」「商品」「会社概要」の三つの項目があります。「商品」は全商品をまとめたページへのリンクで、マウスを重ねると「照明」「椅子」「ラグ」の一覧が下に開きます。マウスの利用者は、一覧へポインターを移してカテゴリーを選ぶことも、「商品」をクリックしてすべての商品を見ることもできます。ポインターは、リンクを押さずにその上で止まっていられるからです。

キーボードには、止まっているという操作がありません。Tabキーで「商品」へ移動してEnterキーを押すと、一覧を開くかページへ移動するかは、サイト側で決めるしかありません。よくある作り方の一つは、1回目と2回目で使い分けるものです。1回目のEnterでは一覧が開いてページはそのまま、2回目でリンク先へ移動します。スクリーンリーダーは押す前も後も「商品、リンク」と読むため、利用者には何も起きなかったように聞こえます。もう一度押すと「商品」のページへ移り、「照明」にはたどり着けません。

もう一つのよくある作り方は、キーボードでメニューを使えるようにするために書かれたものです。Enterキーで一覧が開閉し、リンクは「折りたたみ」「展開」と読まれます。その代わりに、「商品」のページへ行く手段がなくなりました。マウスで「商品」をクリックすればそのページへ移動しますが、キーでは移動できません。スクリーンリーダーを使っていると、逆のことも起きます。ブラウズモードでは、スクリーンリーダーがEnterキーをページに渡さずにリンクを自分でクリックすることが多いため、利用者は「商品」のページへ移動し、一覧を目にすることはありません。

## 確認できる問題

リンクがリンクとして読み上げられるのは、押すとどこかへ移動するからです。ここでは1つのリンクが、ページへの移動と一覧の開閉という二つの役目を持っています。どちらか一方だけを選べるのはポインターだけで、重ねれば一覧が開き、クリックすればページへ移動します。これらの作り方では、押したときにどちらの役目を果たすかをスクリプトが決めており、その決まりはマークアップのどこにも書かれていません。

WCAG 2.1.1 キーボードは、ページでできることをすべてキーボードでもできるよう求めています。二つ目の作り方は、この基準を満たしません。キーのハンドラーが「商品」でのEnterを取り消して一覧を開く一方、マウスのクリックはリンク先へ移動するため、「商品」のページはマウスなら1回のクリックで開けるのに、キーボードでは届きません。一つ目の作り方は、2回目のEnterでページに届くため、2.1.1は満たします。

4.1.2 名前・役割・値は、コントロールの役割と状態を読み取れること、そしてその変化が伝わることを求めています。一つ目の作り方では、「商品」は一覧を開くので、開いているか閉じているかという状態を持ちますが、それを示すものがありません。スクリーンリーダーは一覧が開いていてもいなくても「商品、リンク」と読みます。二つ目の作り方では、`aria-expanded` で状態は伝わりますが、役割はリンクのままで、そのリンクはEnterキーでリンクとして働きません。「商品、リンク、折りたたみ」から分かるのは何かが開くということだけで、Enterキーではページへ移動できないことは分かりません。また、ブラウズモードのスクリーンリーダーはたいていEnterキーをリンクのクリックに置き換え、キーのハンドラーは動かないため、何が起きるかはスクリーンリーダーを使っているかどうかで変わります。

## 利用者への影響

スマートフォンでタップしたときの動作は、スクリプトだけでなくブラウザーにも左右されます。最初のタップで一覧を開き、2回目でリンク先へ移動するブラウザーもあれば、すぐに移動するブラウザーもあります。同じヘッダーが利用者によって三通りに動き、そのどれもが前もって伝えられません。

## 自動チェックとAIの限界

自動チェックが読むのは `<a href="/products">` という、名前のある正しいリンクです。`aria-expanded` もリンクに付けてよい属性なので、何も指摘されません。Enterキーを押さないため、リンクがどこへも移動しないことや、2回目でしか移動しないことには気づきません。ドロップダウンをキーボードで使えるようにする方法として、リンク先へ移動させずにEnterキーでサブメニューを開くよう勧める記事もあります。AIのコーディングツールに同じことを頼むと、二つ目の作り方そのものを書くことがあります。リンクにキーのハンドラーを付けてEnterを取り消し、一覧を開き、`aria-expanded` や `aria-haspopup` を加えるものです。このコードは自動チェックを通りますが、問題を一覧からページへ移しただけです。

この診断ツールは、サブメニューを持ち、横にボタンのない上段の各リンクでEnterキーを押します。その際、リンク先への移動は止め、ページはそのままにします。Enterキーで一覧が開き、リンク先へは一度も移動しない一方、マウスのクリックでは移動する場合は、問題として報告します。1回目のEnterで一覧が開き2回目でリンク先へ移動する場合や、どの操作でもリンク先へ移動しない場合は、確認が必要な項目として報告します。ページが何を伝えているか、「商品」のページがほかの場所からリンクされているかは、人が判断する必要があるからです。どちらの場合も、確かめるにはマウスを使わずにヘッダーをTabキーで移動し、さらにスクリーンリーダーのブラウズモードでも試します。

## 修正前：1回目で一覧が開き、2回目でリンク先へ移動する

```html
<nav aria-label="メイン">
  <ul class="menu">
    <li>
      <a href="/">ホーム</a>
    </li>
    <li class="has-sub">
      <a href="/products">商品</a>
      <ul>
        <li>
          <a href="/products/lamps">照明</a>
        </li>
        <li>
          <a href="/products/chairs">椅子</a>
        </li>
        <li>
          <a href="/products/rugs">ラグ</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/about">会社概要</a>
    </li>
  </ul>
</nav>
```

```css
.has-sub { position: relative; }
.has-sub > ul { display: none; position: absolute; top: 100%; left: 0; }
.has-sub.open > ul { display: block; }
```

```js
for (const item of document.querySelectorAll('.has-sub')) {
  const link = item.querySelector(':scope > a');
  const setOpen = (open) => item.classList.toggle('open', open);
  item.addEventListener('mouseenter', () => setOpen(true));
  item.addEventListener('mouseleave', () => setOpen(false));
  link.addEventListener('click', (event) => {
    if (item.classList.contains('open')) return;
    event.preventDefault();
    setOpen(true);
  });
}
```

マウスではポインターを重ねた時点で一覧が開くため、クリックすれば必ずリンク先へ移動します。リンクでEnterキーを押したときも同じclickイベントが発生するため、ここで問題になります。

- 「商品」での1回目のEnterは取り消され、代わりに一覧が開きます。
- 「商品」は押す前も後も「商品、リンク」と読まれるため、一覧が開いたことも、次のEnterでページを離れることも伝わりません。

## 修正前：Enterキーでは一覧が開くだけで、マウスのクリックでしかリンク先へ移動しない

```html
<nav aria-label="メイン">
  <ul class="menu">
    <li>
      <a href="/">ホーム</a>
    </li>
    <li class="has-sub">
      <a href="/products" aria-expanded="false">商品</a>
      <ul>
        <li>
          <a href="/products/lamps">照明</a>
        </li>
        <li>
          <a href="/products/chairs">椅子</a>
        </li>
        <li>
          <a href="/products/rugs">ラグ</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/about">会社概要</a>
    </li>
  </ul>
</nav>
```

```css
.has-sub { position: relative; }
.has-sub > ul { display: none; position: absolute; top: 100%; left: 0; }
.has-sub.open > ul { display: block; }
```

```js
for (const item of document.querySelectorAll('.has-sub')) {
  const link = item.querySelector(':scope > a');
  const setOpen = (open) => {
    item.classList.toggle('open', open);
    link.setAttribute('aria-expanded', String(open));
  };
  item.addEventListener('mouseenter', () => setOpen(true));
  item.addEventListener('mouseleave', () => setOpen(false));
  link.addEventListener('keydown', (event) => {
    if (event.key !== 'Enter') return;
    event.preventDefault();
    setOpen(!item.classList.contains('open'));
  });
}
```

クリックのハンドラーをやめ、代わりにキーのハンドラーで一覧を開き、`aria-expanded` も合わせて切り替えています。Enterキーで「照明」「椅子」「ラグ」に届くようになりましたが、次の問題があります。

- 「商品」でのEnterは一覧を開閉するだけです。マウスのクリックなら開く「商品」のページに、キーでは届きません。
- 「商品」はリンクのままで、「商品、リンク、折りたたみ」と読まれるため、Enterキーでそのページへ移動しないことは伝わりません。
- ブラウズモードのスクリーンリーダーはたいていEnterキーを押す代わりにリンクをクリックし、キーのハンドラーは動きません。利用者は「商品」のページへ移動し、一覧は開きません。

## 修正後：リンクと、その横のボタン

```html
<nav aria-label="メイン">
  <ul class="menu">
    <li>
      <a href="/">ホーム</a>
    </li>
    <li class="has-sub">
      <a href="/products">商品</a>
      <button type="button" aria-expanded="false" aria-controls="sub-products" aria-label="商品のページ一覧">▾</button>
      <ul id="sub-products" tabindex="-1" hidden>
        <li>
          <a href="/products/lamps">照明</a>
        </li>
        <li>
          <a href="/products/chairs">椅子</a>
        </li>
        <li>
          <a href="/products/rugs">ラグ</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/about">会社概要</a>
    </li>
  </ul>
</nav>
```

```css
.has-sub { position: relative; }
.has-sub > ul { position: absolute; top: 100%; left: 0; }
.has-sub > button { min-width: 24px; min-height: 24px; }
```

```js
for (const item of document.querySelectorAll('.has-sub')) {
  const button = item.querySelector(':scope > button');
  const list = document.getElementById(button.getAttribute('aria-controls'));
  const setOpen = (open) => {
    button.setAttribute('aria-expanded', String(open));
    list.hidden = !open;
  };
  button.addEventListener('click', () => setOpen(list.hidden));
  item.addEventListener('keydown', (event) => {
    if (event.key === 'Escape' && !list.hidden) {
      setOpen(false);
      button.focus();
    }
  });
  // フォーカスが項目から離れたら閉じ、フォーカスは利用者が移した場所に残す
  item.addEventListener('focusout', (event) => {
    if (!item.contains(event.relatedTarget)) setOpen(false);
  });
}
```

1. 「商品」はハンドラーのないただのリンクに戻します。Enter、クリック、タップのどれでも、1回目で「商品」のページへ移動します。
2. その直後に、シェブロンを描いたHTML標準のbuttonを置きます。Enter、Space、クリックで一覧を開閉できます。
3. buttonには「商品のページ一覧」という名前を付け、スクリーンリーダーや音声操作でリンクと区別できるようにします。大きさは24×24ピクセル以上にします。
4. buttonに `hidden` と連動する `aria-expanded` を付けます。「商品のページ一覧、ボタン、折りたたみ」、開くと「展開」と読み上げられます。`aria-controls` で一覧と結び付けます。
5. Escapeで一覧を閉じてフォーカスをbuttonに戻し、フォーカスが項目から離れたら、フォーカスを動かさずに閉じます。
6. 一覧に `tabindex="-1"` を付けます。一覧の中をクリックしてもフォーカスが項目の内側に残り、離れたとは見なされません。

これで二つの修正前の例はどちらも直り、2.1.1と4.1.2を満たします。各コントロールが一つの役目だけを持ち、それを伝えるからです。W3CのAPGにある、上段のリンクを残したナビゲーションの例（Disclosure Navigation Menu with Top-Level Links）と同じ形です。WordPressのナビゲーションブロックも、サブメニューをマウスを重ねて開く設定にし、矢印を表示していれば、この形で出力されます。

## 修正後：ボタンにして、ページを一覧に移す

```html
<nav aria-label="メイン">
  <ul class="menu">
    <li>
      <a href="/">ホーム</a>
    </li>
    <li class="has-sub">
      <button type="button" aria-expanded="false" aria-controls="sub-products">商品</button>
      <ul id="sub-products" tabindex="-1" hidden>
        <li>
          <a href="/products">すべての商品</a>
        </li>
        <li>
          <a href="/products/lamps">照明</a>
        </li>
        <li>
          <a href="/products/chairs">椅子</a>
        </li>
        <li>
          <a href="/products/rugs">ラグ</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/about">会社概要</a>
    </li>
  </ul>
</nav>
```

```css
.has-sub { position: relative; }
.has-sub > ul { position: absolute; top: 100%; left: 0; }
```

```js
for (const item of document.querySelectorAll('.has-sub')) {
  const button = item.querySelector(':scope > button');
  const list = document.getElementById(button.getAttribute('aria-controls'));
  const setOpen = (open) => {
    button.setAttribute('aria-expanded', String(open));
    list.hidden = !open;
  };
  button.addEventListener('click', () => setOpen(list.hidden));
  item.addEventListener('keydown', (event) => {
    if (event.key === 'Escape' && !list.hidden) {
      setOpen(false);
      button.focus();
    }
  });
  // フォーカスが項目から離れたら閉じ、フォーカスは利用者が移した場所に残す
  item.addEventListener('focusout', (event) => {
    if (!item.contains(event.relatedTarget)) setOpen(false);
  });
}
```

1. 「商品」をHTML標準のbuttonにします。Enter、Space、クリック、タップのどれでも一覧を開閉するだけで、リンクのふりはしません。
2. 「商品」のページへのリンクは、「すべての商品」として一覧の先頭に移します。一覧を開けば、1回の操作で届きます。
3. buttonに `hidden` と連動する `aria-expanded` を付けます。「商品、ボタン、折りたたみ」、開くと「展開」と読み上げられます。`aria-controls` で一覧と結び付けます。
4. Escapeで一覧を閉じてフォーカスを「商品」に戻し、フォーカスが項目から離れたら閉じます。
5. 一覧に `tabindex="-1"` を付け、一覧の中をクリックしても離れたとは見なされないようにします。

これで二つの修正前の例はどちらも直り、2.1.1と4.1.2を満たします。その分の負担はマウスの利用者にかかり、「商品」のページへ行くのに1回ではなく2回クリックすることになります。WordPressのナビゲーションブロックで、サブメニューをクリックで開く設定にしたときに出力される形と同じです。ただし、ブロックは親のページを一覧に移さずに外してしまうため、そのリンクは自分で加えます。

## どの方法を選ぶか

どちらも基準を満たします。お店のカテゴリーや会社概要のように、セクションのページそのものに意味がある場合は、リンクを残してボタンを加えます。誰にとっても1回のクリック、タップ、Enterで届くページのままで、W3Cの例やWordPressのナビゲーションブロックもこの形です。その代わりに項目ごとに小さめの操作対象がもう一つ増え、それには専用の名前と十分な大きさが必要です。項目のページが一覧の中身の目次にすぎない場合は、項目をボタンにします。項目ごとにコントロールが一つ、Tabの停止位置も一つで、押しやすい大きさになり、「商品」のページへのリンクは一覧の先頭に入ります。

## 実装の判断

ボタンには専用の名前が必要です。リンクと同じ名前にすると、「商品、リンク」の次に「商品、ボタン」と、同じものが2回あるように聞こえ、音声操作で「商品をクリック」と言った利用者は二つの候補から選ぶことになります。「商品のページ一覧」のように、何が開くかを表す名前にします。シェブロンはリンクの文字の中ではなくボタンの中に描き、一覧を開く操作対象にします。リンクの文字の中に描くと、それを押した利用者はページへ移動してしまいます。

実際のメニューでは、ほかの部分についても判断が必要です。

- マウスを重ねて開く操作：マウス利用者のために残してもかまいません。その場合は同じ `setOpen` で状態を設定し、一覧が表示されている間は `aria-expanded` をtrueに保ちます。また1.4.13に従い、ポインターを一覧へ移しても開いたままにし、Escapeで閉じます。詳しくは[近づくと消え、閉じられないツールチップ](/ja/learn/hover-content)を参照してください。
- キーのハンドラー：リンクの `keydown` ハンドラーで一覧を開かないでください。ブラウズモードのスクリーンリーダーはキーではなくクリックを送ることが多いため、ハンドラーが動く利用者と動かない利用者が出てしまいます。
- `aria-haspopup`：リンクにもボタンにも付けません。メニューとして読み上げられますが、この一覧はリンクの集まりで、メニューではありません。
- ターゲットサイズ：シェブロンのボタンは小さくなりがちです。24×24ピクセル未満なら周りに余白が必要です。[ターゲットサイズが足りない、詰まったボタン](/ja/learn/target-size)で説明しています。
- スマートフォン：狭い画面のメニューでも同じリンクとボタンが使え、一覧は縦に並んで次の項目を押し下げます。狭い画面だけ、最初のタップで一覧を開く決まりに切り替えることはしません。

## 修正の確認

1. マウスを使わずにナビゲーションをTabキーで移動し、上段の各リンクでEnterキーを押す。どのリンクも1回目でそのページへ移動し、一覧を開くリンクがないことを確認する。
2. 各サブメニューのbuttonへTabキーで移動し、EnterとSpaceで開いて、一覧の中へTabキーで進む。開閉のたびに `aria-expanded` が画面上の一覧と一致することを確認する。
3. 一覧を開いた状態でEscapeを押し、一覧が閉じてフォーカスがbuttonにあることを確認する。もう一度開き、TabやShift + Tabで項目から離れたとき、一覧が閉じ、フォーカスが移した場所に残ることを確認する。
4. スクリーンリーダーのブラウズモードで、矢印キーでナビゲーションを読み進め、各リンクと各buttonでEnterキーを押す。リンクはそのページへ移動し、buttonはボタンとして、専用の名前と展開または折りたたみの状態とともに読み上げられることを確認する。
5. タッチ画面で、上段の各リンクとそのbuttonをそれぞれ1回タップする。200%の拡大表示で、どちらも画面に表示され、それぞれ自分の役目だけを果たすことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。共通のヘッダーやメニューのコンポーネントを変更した際は、再度確認します。

## 制限

このガイドで扱うのは、リンクでありながらサブメニューも開く上段の項目です。スクリプトもボタンもなくマウスを重ねたときだけ開くサブメニューや、一覧が開いているかどうかを伝えないボタンは、[マウスでしか開かないか、開閉が伝わらないドロップダウンメニュー](/ja/learn/nav-menus)で扱っています。コマンドを並べるアプリケーションのメニューは、代わりにARIAのメニューパターンに従います。スクリプトは、アニメーション、マウスを重ねて開く操作、入れ子の階層を含まない最小限の例です。狭い画面のメニューも含めて、実際に使用する支援技術で操作全体を確認してください。

## スクリーンリーダーでの動き（EaseWebが実機で確認）

`aria-expanded`: ボタンが開くメニューやセクションが、開いているかどうかを示します。

| スクリーンリーダー | ブラウザーとOS | 押す | Tabで移動 |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

https://easeweb.dev/ja/learn/screen-reader-support

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
