Webサイトでよく使われるアコーディオン機能。今回は、クリック時とホバー時の両方に対応し、かつdetailsタグでもWordPressメニューでも使える汎用性の高いアコーディオンを目指しましたにホバー型では素早いマウス操作にも対応します。
完成デモ
Clickdion(クリック型)
.openerボタンをクリックで開閉- 連打防止機能付き
- summaryとbutton両方のパターンに対応
Hoverdion(ホバー型)
- マウスホバーで自動開閉
- 素早いマウス移動にも即座に反応
- アニメーション中断機能で快適な操作感
主な特徴
1. detailsタグでスムーズなアニメーション
通常、<details>タグは開閉時にアニメーションが適用されませんが、Web Animations APIを使用することで滑らかな動作を実現しています。
<div class="clickdion">
<details class="dion-item">
<summary class="dion-summary">
クリックして開閉<button class="opener"></button>
</summary>
<div class="dion-content">
<!-- コンテンツ -->
</div>
</details>
</div>
2. WordPressメニューにも対応
WordPressの標準的なメニュー構造にも対応。既存のテーマを大きく変更することなく導入できます。
<ul class="hoverdion">
<li class="menu-item menu-item-has-children">
<a href="#" class="menu-link">
親メニュー<button class="opener"></button>
</a>
<ul class="sub-menu">
<li class="menu-item">
<a href="#" class="menu-link">子メニュー1</a>
</li>
<li class="menu-item">
<a href="#" class="menu-link">子メニュー2</a>
</li>
</ul>
</li>
</ul>
3. 柔軟なトリガー設定
.openerクラスを使い分けることで、様々なUIパターンに対応:
summaryに直接.openerを付ける → summary全体がトリガーsummary内のbuttonに.openerを付ける → ボタンのみがトリガー
改良されたホバー機能
従来の問題点
一般的なホバー型アコーディオンでは、以下の問題がありました:
- 素早いマウス移動で反応が鈍い
- アニメーション中に操作が無視される
- 連打防止機能がホバーを阻害
解決策
クリック型とホバー型で異なるアプローチを採用:
// クリック型:連打防止あり
const setupClickAccordion = (element) => {
opener.addEventListener("click", (e) => {
// 連打防止
if (isAnimating(element)) return;
toggleAccordion(element);
});
};
// ホバー型:即座に反応、アニメーション中断機能
const setupHoverAccordion = (element) => {
summary.addEventListener('mouseenter', () => {
if (!isOpen(element)) {
// アニメーション中でも強制的に開く
openAccordion(element, true);
}
});
};
アニメーション中断機能
既存のアニメーションを中断して、新しい操作に即座に反応:
const openAccordion = (element, force = false) => {
// 既存のアニメーションを中断(force時)
if (force && isAnimating(element)) {
const existingAnim = element._currentAnimation;
if (existingAnim) {
existingAnim.cancel();
}
}
// 新しいアニメーション開始
animAccordionContent(content, 0, content.scrollHeight, element);
};
完全なJavaScript実装
(function() {
// CSS自動注入
const STYLE = document.createElement('style');
STYLE.textContent = `
:is(.clickdion, .hoverdion) :is(
.dion-item .dion-content,
.menu-item-has-children .sub-menu) {
overflow: hidden;
transition: all 0.3s ease-out;
}
:is(.clickdion, .hoverdion) :is(
.dion-item:not(.is-open) .dion-content,
.menu-item-has-children:not(.is-open) .sub-menu) {
height: 0;
min-height: 0;
padding-block: 0;
border-block-width: 0;
}
/* アイコンスタイル */
.opener::before {
content: "▼";
transition: transform 0.3s ease;
}
.is-open .opener::before {
transform: rotate(-180deg);
}
`;
document.head.appendChild(STYLE);
// 初期化
document.addEventListener("DOMContentLoaded", () => {
// details要素
document.querySelectorAll(".clickdion .dion-item").forEach(setupClickAccordion);
document.querySelectorAll(".hoverdion .dion-item").forEach(setupHoverAccordion);
// メニュー要素
document.querySelectorAll(".clickdion .menu-item-has-children").forEach(setupClickMenudion);
document.querySelectorAll(".hoverdion .menu-item-has-children").forEach(setupHoverMenudion);
});
// クリック型設定(連打防止あり)
const setupClickAccordion = (element) => {
const opener = element.querySelector(".opener");
const summary = element.querySelector(".dion-summary");
if (!opener) return;
opener.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
if (isAnimating(element)) return; // 連打防止
toggleAccordion(element);
});
summary?.addEventListener("click", (e) => e.preventDefault());
};
// ホバー型設定(即座に反応)
const setupHoverAccordion = (element) => {
const summary = element.querySelector(".dion-summary");
const content = element.querySelector(".dion-content");
if (!summary || !content) return;
summary.addEventListener("click", (e) => e.preventDefault());
['mouseenter', 'focus'].forEach(event => {
summary.addEventListener(event, () => {
if (!isOpen(element)) {
openAccordion(element, true); // 強制実行
}
});
});
['mouseleave', 'blur'].forEach(event => {
element.addEventListener(event, () => {
if (isOpen(element)) {
closeAccordion(element, true); // 強制実行
}
});
});
};
// アニメーション実行
const animAccordionContent = (content, fromHeight, toHeight, element, onComplete) => {
element.dataset.animStatus = "action";
const animation = content.animate([
{ height: fromHeight + 'px' },
{ height: toHeight + 'px' }
], {
duration: 300,
easing: "ease-out"
});
element._currentAnimation = animation;
animation.onfinish = () => {
element.dataset.animStatus = "";
element._currentAnimation = null;
onComplete?.();
};
};
// ユーティリティ関数
const isAnimating = (element) => element.dataset.animStatus === "action";
const isOpen = (element) => element.classList.contains("is-open");
})();
CSS設定のポイント
基本的なアコーディオン構造
/* 閉じた状態 */
.dion-content,
.sub-menu {
height: 0;
overflow: hidden;
padding-block: 0;
border-block-width: 0;
}
/* 開いた状態 */
.is-open .dion-content,
.is-open .sub-menu {
height: auto;
padding-block: initial;
border-block-width: initial;
}
Material Symbols使用時
.opener::before {
font-family: "Material Symbols Sharp";
font-variation-settings: "FILL" 0, "wght" 200;
content: "\e313"; /* expand_more */
transition: rotate 0.3s ease;
}
.is-open .opener::before {
rotate: -180deg;
}
カスタマイズ時のポイント
1. アニメーション速度の調整
// より高速な反応が欲しい場合
duration: 200
// よりゆっくりとした動作が欲しい場合
duration: 500
2. イージングのカスタマイズ
// 自然な動作
easing: "ease-out"
// バウンド効果
easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)"
// リニア
easing: "linear"
3. 排他制御の追加
同時に開けるアコーディオンを1つに制限:
const openAccordion = (element, force = false) => {
// 同じ親要素内の他のアコーディオンを閉じる
const parent = element.closest('.accordion-group');
if (parent) {
parent.querySelectorAll('.is-open').forEach(sibling => {
if (sibling !== element) {
closeAccordion(sibling, true);
}
});
}
// 通常の開く処理...
};
4. WordPressテーマでの実装
// functions.phpに追加
function enqueue_accordion_script() {
wp_enqueue_script(
'accordion-js',
get_template_directory_uri() . '/js/accordion.js',
array(),
'1.0.0',
true
);
}
add_action('wp_enqueue_scripts', 'enqueue_accordion_script');
// メニューにクラスを追加
function add_accordion_classes($classes, $item, $args) {
if (in_array('menu-item-has-children', $classes)) {
$classes[] = 'hoverdion'; // または 'clickdion'
}
return $classes;
}
add_filter('nav_menu_css_class', 'add_accordion_classes', 1, 3);
パフォーマンス最適化
1. CSS containment
.dion-content,
.sub-menu {
contain: layout style paint;
}
2. will-change プロパティ
.dion-content,
.sub-menu {
will-change: height;
}
/* アニメーション完了後は削除 */
.dion-content:not(.animating),
.sub-menu:not(.animating) {
will-change: auto;
}
3. アニメーションの最適化
// アニメーション開始時
content.style.willChange = 'height';
// アニメーション完了時
animation.onfinish = () => {
content.style.willChange = 'auto';
// その他の処理...
};
トラブルシューティング
よくある問題と解決策
Q: ホバー時に素早く移動すると反応しない
A: 本実装ではforceパラメータでアニメーション中断機能を実装済み
Q: Material Symbolsが表示されない
A: フォントの読み込みを確認:
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp" rel="stylesheet">
Q: WordPressでメニューが動作しない
A: メニュー構造とクラス名を確認。必要に応じてPHPでクラスを追加
まとめ
今回作成したアコーディオンの特徴:
- 用途別最適化:クリック型は安定性、ホバー型は反応速度を重視
- アニメーション中断:素早い操作にも即座に対応
- 汎用性:detailsタグとWordPressメニューの両方に対応
- 自動CSS注入:JavaScriptファイル1つで完結
- カスタマイズ性:豊富な設定オプション
様々な要件に対応できるよう柔軟性を重視、特にWordPressの標準メニュー構造でホバーでもクリックでも使えるアコーディオンメニューです。

