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-childやparent-itemなど)を前提にしている。
そのJSをコピペすると、querySelectorAll('.has-child')は0件を返し、クリックイベントがどこにも発火しません。
「JSの書き方は間違っていないのに動かない」という状況のほとんどはここが原因です。
過去のWordPressカスタムテーマ案件でまさにこの状況に遭遇しました。
DevToolsでナビゲーション部分を確認したところ、<li>にはmenu-item menu-item-has-childrenが付いていた。
コピペ元JSが対象にしていたクラス名とは一致しておらず、バインド先を変えるだけで動き出しました。
確認手順は次のとおりです。
- ブラウザのDevToolsを開く(F12)
- ドロワーメニューのサブメニューを持つ<li>要素を選択する
- クラス属性に
menu-item-has-childrenがあるかを確認する - 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=""/about/">会社概要</a>"
<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ステップ診断と直し方も参考にしてください。