本文へスキップ
Build

Column

コラム

制作者向け / 更新

WordPress自作テーマのドロワーアコーディオン——バニラJS実装

この記事には広告リンクを含みます。紹介している商品・サービスの一部はアフィリエイトプログラムを利用しています。 商品・サービスの選定はご自身の判断でお願いいたします。

WordPress自作テーマのドロワーアコーディオン——バニラJS実装 のアイキャッチ

WordPressのカスタムテーマにドロワーアコーディオンを追加しようとして、コピペしたJSが動かない——この状況には理由があります。
動かない原因は2点にほぼ絞られます。
1つ目はwp_nav_menu()が出力するクラス名とJSのバインド先のズレ、2つ目は<a>の内側に<button>を置くHTML構造の問題です。
この2点を押さえることで、jQueryなしでwp_nav_menu()出力に合ったアコーディオンを実装できます。

wp_nav_menu()の出力クラスとコピペJSがズレる理由

動かない直接の原因はクラス名の不一致にあります。
wp_nav_menu()はサブメニューを持つ<li>にmenu-item-has-childrenを自動付与します。
サブメニューの<ul>に付くのがsub-menuクラス。
カスタムwalkerでクラスを上書きしていない限り、この2つがwp_nav_menu()の基本出力クラスです。

クラス名

付与条件

menu-item-has-children

サブメニューを持つ<li>に自動付与

sub-menu

サブメニューの<ul>に自動付与

current-menu-item

現在表示中ページの<li>に付与

Web上のアコーディオンチュートリアルの多くは、開発者が自分で命名した任意クラス(has-childparent-itemなど)を前提にしている。
そのJSをコピペすると、querySelectorAll('.has-child')は0件を返し、クリックイベントがどこにも発火しません。
「JSの書き方は間違っていないのに動かない」という状況のほとんどはここが原因です。

過去のWordPressカスタムテーマ案件でまさにこの状況に遭遇しました。
DevToolsでナビゲーション部分を確認したところ、<li>にはmenu-item menu-item-has-childrenが付いていた。
コピペ元JSが対象にしていたクラス名とは一致しておらず、バインド先を変えるだけで動き出しました。

確認手順は次のとおりです。

  1. ブラウザのDevToolsを開く(F12)
  2. ドロワーメニューのサブメニューを持つ<li>要素を選択する
  3. クラス属性にmenu-item-has-childrenがあるかを確認する
  4. JSのバインド先クラスをここに合わせる

functions.php側でwp_nav_menu()の引数を変えてもクラス名には影響しません。
クラスを変えたい場合はカスタムwalkerが必要です。
wp_nav_menu()の出力カスタマイズ全般はwp_nav_menuでliタグとaタグにクラスを追加する方法も参考にしてください。

<a>の内側に<button>を置くとHTML仕様違反になる

多くのチュートリアルJSは、<a>の内側にボタン要素をappendChildする構造を採用しています。
これはHTML仕様違反——フォーカス管理が壊れる直接の原因。
HTMLの「透過的コンテンツモデル」では、<a>はインタラクティブコンテンツ(<button>・。 <input>等)を子要素に持てません。

ブラウザが自動修復しようとして描画が崩れたり、Tabキーによるフォーカス移動の順番が想定外になったりします。
スクリーンリーダーでも意図しない読み上げが発生することがあります。

正しい構造はトグルボタンを<a>の隣接兄弟要素として配置することです。
<a>のhref先への遷移と、サブメニューの開閉を別の要素に分離します。

<li class="menu-item menu-item-has-children">
  <a href="&quot;/about/&quot;&gt;会社概要&lt;/a&gt;"
  <button type="button" class="accordion-toggle" aria-expanded="false">
    <span class="visually-hidden">サブメニューを開閉する</span>
  </button>
  <ul class="sub-menu">...</ul>
</li>

aria-expandedをボタンに付けると、スクリーンリーダーが開閉状態を読み上げられる。
閉じているときfalse、開いているときtrueにJSで切り替えます。
付けなくてもJSの動作自体は成立しますが、スクリーンリーダーユーザーは開閉状態を確認できません。

visually-hiddenクラスは、ボタンの説明テキストを視覚的には非表示にしつつスクリーンリーダーに読み上げさせる定番パターンです。
CSSでposition: absolute; width: 1px; height: 1px; overflow: hidden;を当てます。
アイコンのみのボタンで広く使われるパターン。

バニラJSとCSSで実装するコード全文

wp_nav_menu()の出力に合わせたバニラJS実装を示します。
jQueryを使わないため、WordPressの読み込み順エラーや二重読み込み問題とは無縁です。

document.addEventListener('DOMContentLoaded', function () {
  var parents = document.querySelectorAll('.menu-item-has-children');

  parents.forEach(function (li) {
    var link = li.querySelector(':scope > a');
    var subMenu = li.querySelector(':scope > .sub-menu');
    if (!link || !subMenu) return;

    var toggle = document.createElement('button');
    toggle.type = 'button';
    toggle.className = 'accordion-toggle';
    toggle.setAttribute('aria-expanded', 'false');
    toggle.innerHTML = '<span class="visually-hidden">サブメニューを開閉する</span>';
    link.insertAdjacentElement('afterend', toggle);

    toggle.addEventListener('click', function () {
      var expanded = toggle.getAttribute('aria-expanded') === 'true';
      toggle.setAttribute('aria-expanded', String(!expanded));
      li.classList.toggle('is-open');
    });
  });
});

insertAdjacentElement('afterend', toggle)で<a>の直後にボタンを挿入しています。
<a>の内側には追加しないため、HTML仕様を守った構造。
:scope > aは<li>直下の<a>のみを対象にし、ネストされたサブメニュー内の<a>には影響しません。

サブメニューが存在しない<li>には早期リターンしているため、余分なボタンは追加されません。
このJSの記述先は、functions.phpでエンキューする外部JSファイル。
wp_enqueue_script()の第5引数をtrueにするとフッターで読み込まれ、DOM構築後に実行されます。

CSSはmax-heightトランジションで開閉アニメーションを実現します。

.sub-menu {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}
.menu-item-has-children.is-open > .sub-menu {
  max-height: 500px;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
}
.accordion-toggle::after {
  content: '▶';
  display: inline-block;
  transition: transform 0.2s;
}
.accordion-toggle[aria-expanded="true"]::after {
  transform: rotate(90deg);
}

矢印の回転はaria-expanded="true"セレクタで制御するため、JSで別途クラスを追加する必要がありません。
display: noneではなくmax-heightを使う理由は、CSSトランジションがdisplayプロパティに対応していないためです。
サブメニューの項目数が多い場合はmax-height: 500pxの値を大きめに調整してください。

まとめ

  • wp_nav_menu()はサブメニューの<li>にmenu-item-has-childrenを自動付与する
  • コピペJSのバインド先クラスをこの名前に合わせると動き出す
  • トグルボタンの配置は<a>の内側でなく隣接兄弟(HTML仕様)
  • aria-expandedでスクリーンリーダーに開閉状態を通知する
  • アニメーションはmax-height transitionとaria-expandedセレクタで完結

コーディング代行・実装判断で詰まったら Build に振ってください。
jQuery実装のまま動かしたいケースはjQueryのハンバーガーメニューが動かない:3ステップ診断と直し方も参考にしてください。