アコーディオンメニューを jQuery なしで : CSS をベースにミニマル Vanilla JS で実装
アコーディオンメニューを jQuery なしで : CSS をベースにミニマル Vanilla JS で実装
たくさんの製品をカテゴリーごとにまとめたり、FAQで詳しい回答をたたんでおいたり、項目を整理して見せるためのアコーディオンメニュー。その実装には、jQuery ライブラリの slideToggle() メソッドがよく使われます。
ですが、jQuery の使用により、パフォーマンス低下や他のフレームワークとの競合が生じることがあります。今回は jQuery に依存せず、CSS をベースに最小限の純粋な JavaScript で実装してみます。
サンプルコード
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>accordion sample</title>
</head>
<body>
<h1>accordion sample</h1>
<dl id="accordion">
<div>
<dt>[Q1]存在するのに犬(居ぬ)とはこれいかに</dt>
<dd>[A1]近寄ってきても猿(去る)と呼ぶが如し</dd>
</div>
<div>
<dt>[Q2]回答が長くても大丈夫ですか?</dt>
<dd>[A2]寿限無、寿限無、五劫のすりきれ、海砂利水魚の、水行末・雲来末・風来末、食う寝るところに住むところ、やぶらこうじのぶらこうじ、パイポ・パイポ・パイポのシューリンガン、シューリンガンのグーリンダイ、グーリンダイのポンポコピーのポンポコナの、長久命の長助</dd>
</div>
<div>
<dt>[Q3]ビューポート幅が変わってテキストが折り返されても大丈夫ですか?</dt>
<dd>[A3]安心してください。QやAの高さが変わるとCSS変数が書き換えられます。お試しください。</dd>
</div>
</dl>
</body>
</html>
CSS
@charset "UTF-8";
#accordion {
margin: 0;
}
#accordion > div {
--term-height: 3.5em;
--group-height: 100dvh;
display: flex;
flex-direction: column;
position: relative;
z-index: 0;
overflow: hidden;
max-height: var(--group-height);
transition: 0.3s max-height ease-out;
}
#accordion > div:not(.open) {
max-height: var(--term-height);
}
#accordion dt,
#accordion dd {
padding: 1em;
line-height: 1.5;
}
#accordion dt {
cursor: pointer;
background-color: beige;
}
#accordion dd {
margin: 0;
position: relative;
z-index: -10;
transition: 0.3s transform ease-out;
}
#accordion > div:not(.open) dd {
transform: translateY(-100%);
}
JavaScript
"use strict";
document.addEventListener("DOMContentLoaded", () => {
// accordion
const term = document.querySelectorAll("#accordion dt");
term.forEach((e) => {
const setData = () => {
const termHeight = e.getBoundingClientRect().height;
const groupHeight = e.parentElement.scrollHeight;
e.parentElement.style.setProperty("--term-height", `${termHeight}px`);
e.parentElement.style.setProperty("--group-height", `${groupHeight}px`);
};
setData();
e.addEventListener("click", () => {
setData();
e.parentElement.classList.toggle("open");
});
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentRect.height !== entry.target.oldHeight) {
entry.target.oldHeight = entry.contentRect.height;
setData();
}
}
});
e.oldHeight = e.getBoundingClientRect().height;
e.parentElement.oldHeight = e.parentElement.scrollHeight;
resizeObserver.observe(e);
resizeObserver.observe(e.parentElement);
});
// end of accordion
});
サンプル DEMO
Question をクリック(タップ)すると、対応する Answer が下にスライドして表示されます。
- [Q1]存在するのに犬(居ぬ)とはこれいかに
- [A1]近寄ってきても猿(去る)と呼ぶが如し
- [Q2]回答が長くても大丈夫ですか?
- [A2]寿限無、寿限無、五劫のすりきれ、海砂利水魚の、水行末・雲来末・風来末、食う寝るところに住むところ、やぶらこうじのぶらこうじ、パイポ・パイポ・パイポのシューリンガン、シューリンガンのグーリンダイ、グーリンダイのポンポコピーのポンポコナの、長久命の長助
- [Q3]ビューポート幅が変わってテキストが折り返されても大丈夫ですか?
- [A3]安心してください。QやAの高さが変わるとCSS変数が書き換えられます。お試しください。
コード解説
HTMLでは、<dl> 要素で記述し、id 属性に "accordion" を設定します。<dt> と <dd> のセットは <div> でグループ化します。<dd> は <dt> に対して1個だけにしてください。
以降解説する CSS と JavaScript は、外部ファイルにして読み込むなり、インラインで記述するなりします。
次に CSS では、35行目で <dd> 要素を上に100%ずらしています。transform プロパティを使用しているので、元の位置の高さはそのまま維持されます。class 属性に "open" が追加されると元の位置に戻ります。
兄要素の <dt> の後ろに隠したいので、11-12行目で親要素の <div> を位置基準にした上で、31-32行目で重ね順をネガティブにしています。このままだと <dt> の後ろに <dd> が重なって見えてしまうため、27行目で前面の <dt> に背景色を設定しています。溢れは13行目で隠しています。
ここからがポイントですが、7-8行目で CSS変数を設定しています。<dt> 要素の高さを "--term-height" 、<div> 要素の高さを "--group-height" として、それぞれ初期値を指定しています。実際には次で解説するJavaScript で上書きするので、ここで宣言しておく必要はありません。
そして14行目で <div> の max-height に "--group-height" を、class 属性に "open" が追加されていない場合には18行目で "--term-height" を指定しています。つまり、<div> は(<dd> のない) <dt> だけの高さになり、 "open" class が追加されると <dd> のある本来の高さに戻るわけです。
最後に JavaScript ですが、CSS の解説で登場した "open" class の追加と、CSS変数 "--group-height" と "--term-height" の上書きだけをしています。
7-12行目の関数で <div>(<dt> の親要素)の style 属性にCSS変数を追加しています。<td> 要素の高さを取得するために getBoundingClientRect().height を使用しています。offsetHeight だと小数点以下を取得できませんので、閉じた時に隙間が生じます。<div> 要素の高さの取得には scrollHeight を使用しています。溢れた <dd> を含んだ要素の内容の高さを取得するためです。
この関数を、13行目で DOM構築完了時、14行目で <dt> 要素クリック時、22行目で <div> または <dt> の高さが変わった時に実行しています。28-29行目は、特定要素のリサイズを監視する resizeObserver です。なかなか使用する機会がありませんが、window の resize イベントリスナーよりも効率がよいでしょう。
おわりに
かなり以前に、アコーディオンメニューを CSS だけでできないものかと試行錯誤したことがあったのですが、結局のところ、実際の高さを取得できなければキレイにはできませんでした(なんとなくならできます)。
今なら CSS変数を使えるようになったし、最小限の JavaScript で実装できるのではないかと思い立って、CSSベースでなるべくシンプルになるように考えてみました。
改善点などありましたら、ご指摘いただければうれしいです。Twitter(現 𝕏)は @uncl010 まで(あまり公私分けてないですが)。
それではまた、Happy coding !