Webサイトでよく使われるUIのひとつに「アコーディオンメニュー」があります。コンテンツを折りたたんで見せたいときや、FAQのように必要な部分だけ展開して表示したいときに非常に便利です。
かつてはjQueryを使って実装することが多かったですが、最近ではブラウザの標準APIが充実してきたため、わざわざjQueryを読み込まなくても純粋なJavaScriptで十分に対応できるようになりました。
この記事では、実際に動作するサンプルコードをもとに「アコーディオンメニューの仕組み」と「実装のポイント」を詳しく解説します。
今回作るアコーディオンの仕様
今回の完成イメージは下記となります。
- 初期状態ではコンテンツが閉じている
- ボタンをクリックするとスムーズに開閉する
- 開いているときは「閉じる」、閉じているときは「もっと見る」とラベルが切り替わる
- 閉じるときにはスクロール位置も自然に戻る
シンプルですが、ユーザー体験を考慮した動作になっています。
HTML構造
アコーディオンのHTMLはとてもシンプルです。
|
1 2 3 4 5 6 7 |
<div class="mdAccordion__content"> <div class="top__content"> test </div> </div> <button class="mdAccordion__btn js-mdAccordion__btn"> <span class="mdAccordion__btnText mdAccordion__btnText--off">もっと見る</span> <span class="mdAccordion__btnText mdAccordion__btnText--on">閉じる</span> </button> |
.mdAccordion__content … 折りたたまれるコンテンツ部分
.mdAccordion__btn … 開閉のトリガーとなるボタン
mdAccordion__btnText–off/on … 状態によって表示を切り替えるラベル
この構造を複数並べても、1つずつ独立して動作します。
SCSSでのスタイル定義
次にスタイルです。ここでは高さをアニメーションさせるために、max-height と transition を利用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
.mdAccordion { &__content { max-height: 0; overflow: hidden; transition: max-height 0.5s ease; &.is-open { overflow: visible; } } &__btnText { &--off { display: block; } &--on { display: none; } } &__btn { display: flex; justify-content: center; align-items: center; position: relative; margin: 4rem auto 0; width: 100%; max-width: 34rem; height: 4.8rem; font-size: 1.6rem; color: blue; box-shadow: 0 0 5px rgba(51, 51, 51, 0.5); cursor: pointer; &.is-open { .mdAccordion__btnText { &--off { display: none; } &--on { display: block; } } } &:hover { color: #fff; background-color: blue; } } } |
ここで注目すべきは .mdAccordion__content に設定している max-height。
height だと自動調整が効かず、中身の高さが変わると対応できませんが、max-height を使えば開閉のアニメーションが滑らかになります。
JavaScriptでの実装
肝心の動きはJavaScriptで制御します。ポイントは以下の通りです。
- ボタンをクリックしたら直前の要素(= コンテンツ部分)を取得
- 開いているかどうかを判定
- max-height を切り替えてアニメーション
- 開いているときはスクロール位置を元に戻す
実際のコードは次のようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
// DOMの読み込み完了後に実行 document.addEventListener("DOMContentLoaded", function () { // アコーディオンのボタンをすべて取得 const btns = document.querySelectorAll(".js-mdAccordion__btn"); btns.forEach((btn) => { // ボタン直前の要素(開閉対象コンテンツ) const content = btn.previousElementSibling; // アニメーション中の多重クリック防止用フラグ let isAnimating = false; // OPEN時のスクロール位置を保持 let scrollYWhenOpened = 0; // ボタンクリック時の処理 btn.addEventListener("click", function () { // アニメーション中なら何もしない if (isAnimating) return; isAnimating = true; // 現在OPEN状態かどうか const isOpen = content.classList.contains("is-open"); if (isOpen) { // ===== CLOSE処理 ===== const contentHeight = content.scrollHeight; const startScroll = window.scrollY; const targetScroll = scrollYWhenOpened; // 現在の高さをセットしてから閉じる content.style.maxHeight = contentHeight + "px"; requestAnimationFrame(() => { content.style.maxHeight = "0px"; content.classList.remove("is-open"); btn.classList.remove("is-open"); }); // スクロール位置もアニメーションで戻す const duration = 300; const startTime = performance.now(); function animateScroll(time) { let progress = (time - startTime) / duration; if (progress > 1) progress = 1; const newY = startScroll + (targetScroll - startScroll) * progress; window.scrollTo(0, newY); if (progress < 1) requestAnimationFrame(animateScroll); } requestAnimationFrame(animateScroll); } else { // ===== OPEN処理 ===== // 現在のスクロール位置を保存 scrollYWhenOpened = window.scrollY; // 一度高さを0にしてから開く content.style.maxHeight = "0px"; content.classList.add("is-open"); btn.classList.add("is-open"); const contentHeight = content.scrollHeight; requestAnimationFrame(() => { content.style.maxHeight = contentHeight + "px"; }); } // 高さアニメーション終了後の処理 content.addEventListener( "transitionend", () => { // OPEN後は高さ制限を解除 if (content.classList.contains("is-open")) { content.style.maxHeight = "none"; } // 次の操作を許可 isAnimating = false; }, { once: true } ); }); }); }); |
実装の工夫ポイント
- scrollHeight を利用
要素の中身の高さを取得することで、どんなコンテンツ量でも対応可能です。 - requestAnimationFrame を使ったアニメーション
setTimeout よりも滑らかで、ブラウザに最適化された描画が行えます。 - アニメーション中の多重クリック防止
isAnimating フラグを使うことで、ユーザーが連打してもバグが起きません。
jQueryを使わないメリット
かつてはjQueryを使ってワンライナーでアコーディオンを実装していましたが、今は素のJavaScriptでも十分に短く書けます。
メリットとして下記3点が挙げられます。
- ライブラリ不要 → ページの読み込みが軽くなる
- モダンブラウザ対応 → 今の標準機能で快適に動く
- 依存関係の削減 → メンテナンスが容易
特にパフォーマンスや表示速度を重視するサイトでは、無駄なライブラリを削ることは大きなメリットになります。
まとめ
今回は「jQueryを使わないアコーディオンメニュー」の作り方を解説しました。
もし既存のサイトでjQueryに依存している部分が多いなら少しずつ置き換えてみるのもおすすめです。
特にアコーディオンのような単純なUIであれば、純粋なJavaScriptの方が軽量で将来的にも安心です。