Tech

JSを最低限しか使わない!でも動くアコーディオンの作り方

最終更新:2026.06.08

はじめに

ヘッダー部をクリックすることで続く詳細部の内容を開閉させることのできるUI、いわゆるアコーディオンは、その見た目や機能から想像するよりは遥かに実装が厄介なUIコンポーネントの一つです。

ただ「開閉する=折りたたむだけ」ならば大した問題はなく、それこそHTMLには <details><summary> という折りたたみ要素を作成するためのタグが存在します。以下のように記載するだけで各ブラウザ標準のUIとして簡易な折りたたみ要素=アコーディオンの機能が成立します。

<details>
  <summary>ヘッダー部</summary>
  ここに詳細が入りますここに詳細が入りますここに詳細が入ります...
</details>

<details>: 詳細折りたたみ要素 – HTML | MDN

持たせたい機能としてはこれだけで事足りてはいるはずなのですが、「アコーディオン」としてのリッチなUI表現で 「開閉時にアニメーションを付けたい」という要望 があれば、それだけでこの <details><summary> はほぼほぼ実質使用することができなくなってしまいます。

理由については以下の記事などを参照していただければと思いますが、主に詳細の高さを自動計算する height: auto; とCSSアニメーション transition の相性が悪く、その解決策として新たに用意されたプロパティも、直近では利用できる環境が限られており動作が安定しないためです。

detailsとsummaryタグで作るアコーディオンUI – アニメーションのより良い実装方法 – ICS MEDIA

2025年5月時点ではChromeとEdgeが完全に対応しています。Safariはinterpolate-size: allow-keywords;heightプロパティの値を0からautoへ変換を可能にさせるための設定)が未対応のため、固定数値へのアニメーション(※)をフォールバックとして指定しています。Firefoxは未対応のためアニメーションされません。

上記の記事では「Javascriptで開閉アニメーションをつける方法(全ブラウザー対応)」として、HTML構造は変えずにJSでアニメーションを付ける例が紹介されているので、はじめからこれらの <details> <summary> を使う想定で実装を進めるのであればそれでよいかもしれません。

ただし今回はタイトルの通り、アニメーションを付けるためだけのJSにはほとんど頼らない形で実装する方が好ましいのでは?と考えた方法になります。 もちろん別解としてjQueryの slideToggle() がなんか余白周りも含めてうまいことやってくれるのは百も承知ですが、実装者である以上その内訳はある程度知っておかねばなりません。

また、アクセシビリティ観点にも長けた専用の要素が用意されていたとしても、(このデザインを再現するためには使い物にならない……)というのはWeb制作あるあるかと思いますので、可能な限り汎用的かつ、その気になればなんだってできるJSには頼らない形での実装を目指しました。

構造

HTML

<div id="accordion" class="js-accrodion">
 <p class="summary"><!-- ヘッダー部のため、場合に応じてh1〜h6に変更可能 -->
    <button id="accordionBtn" type="button" class="summary-btn js-accrodionBtn"
    aria-controls="accordionDetail" aria-expanded="false">
      ヘッダー部
    </button><!-- /#accordionBtn.summary-btn -->
  </p><!-- /.summary -->
  <div id="accordionDetail" class="details js-accrodionDetail"
    aria-labelledby="accordionBtn" aria-hidden="true"><!-- aria-hiddenはinertでも可 -->
    <div class="details-wrapper">
      <div class="details-content">
        <p>ここに詳細が入りますここに詳細が入りますここに詳細が入ります...</p>
      </div><!-- /.details-content -->
    </div><!-- /.details-wrapper -->
  </div><!-- /#accordionDetail.details -->
</div><!-- /#accordion -->

クラス名を仮に <details><summary> の関係に合わせてはいますが、環境に合わせて調整ください。そちらのシンプルさと比べると早速ごちゃごちゃした構造になってしまいましたが……、 .details からの三階層にも深くわたる <div> 乱舞は「アニメーションをさせるため」に最低限必要な経費となります。

また今回の例では状態を表すためにWAI-ARIA( aria-◯◯ 属性)を利用しているため、必然的に各パーツにIDによるラベル付けを行っています。実際の状態とスタイルを一致させることができるため便利でもあるのですが、慣れていなければ少しややこしいかと思います。


(実際「拡張しているかどうか」の状態を表す aria-expandedfalse の時に「 表示かどうか」を表す aria-hiddentrue とあべこべになっているのは正直わかりづらくて混乱しがちです……しますよね?)

※わかりやすさならば aria-hidden="true"hidden 属性として代替も可能かと思いますが、おそらくリセットCSS等で [hidden] { display: none; } が指定されていることも多いかと思うので、置き換える場合はtransitionによるアニメーションに支障が出ないよう注意して @starting-style などの併用も検討してください。

追記:これらの扱いについてClaudeに相談してみたところ、動作上問題ないが aria-hidden よりは .is-open のような切り替えクラスを使ったほうがアクセシビリティの慣習的には好ましいかつ、 aria-expanded で開閉状態は伝わるので少し冗長かもということでしたが、中間案としてパネル側の非活性状態の切り替えには inert 属性を利用するのがよい かもとのことでした。特にアコーディオン内にフォーカス可能な要素が含まれる場合は、「見えない要素にフォーカス」が当てられるのを避けられるようです。


それでは、CSSと合わせて各部の役割を説明していきます。

CSS

あくまでアニメーションに関するもののみで、細かいボタンのスタイルなどは一部省略します。 ※ #accordion はヘッダー部〜詳細部のグループ化のためにあるので、スタイルは設定しません。

/* ヘッダー部 */
.summary { /* ここに余白は付けない */ }
.summary-btn { /* ヘッダー部の幅・高さいっぱい広げて内部を中央寄せ */
  display: inline-flex;
  align-items: center;
  justify-content: center; /* 左寄せ文字にしたければflex-start */
  width: 100%;
  height: 100%;
  /* 以下ヘッダー部ボタン用のスタイル(省略) */
}
.summary-btn[aria-expanded="false"] { /* クローズ時のヘッダー部ボタン(省略) */ }
.summary-btn[aria-expanded="true"] { /* オープン時のヘッダー部ボタン(省略) */ }

/* 詳細部 */
.details {
  display: grid;
  transition: grid-template-rows 0.3s ease;
}
.details[aria-hidden="true"] { /* クローズ時の詳細部、.details[hidden]でも可 */
  /* display: grid !important; */
  grid-template-rows: 0fr;
}
.details[aria-hidden="false"] { /* オープン時の詳細部、.details:not([hidden])でも可 */
  /* display: grid !important; */
  grid-template-rows: 1fr;
}
.details-wrapper { /* クローズ時の余剰分を隠す */
  overflow: hidden;
}
.details-content { /* オープン時のヘッダー部と詳細部の余白を付ける */
  padding-top: 20px;
}

高さを自動計算する height: auto; とCSSアニメーション transition の相性が悪く

こちらの理由により高さ height は使用せず、代わりにグリッドアイテムとしての grid-template-rows0fr1frtransition させることでアニメーションを再現します。

grid-template-rows とは本来、縦向き=行が積まれる方向にグリッドを分割するために使われるものですが、与える指定を1件(分割無し)・かつ 1fr のみとすることで、 grid-template-rows: auto; (≒ height: auto )を指定した場合と同じ挙動になります。

このように0からautoのような遷移は難しくとも、0(fr)〜1(fr)のように数値が明示できるものであればCSSによる transition(遷移)は問題なく働いてくれるため、この習性を利用しています。

三階層にも深くわたる <div> 乱舞は「アニメーションをさせるため」に最低限必要

またこちらについても、より詳細に各 <div> 要素の役割をそれぞれ示すと以下になります。

  • .detailsアニメーション担当レイヤー
    • 直下要素 .details-wrapper のグリッドアイテム化( display: grid;
    • アコーディオン詳細部のCSSアニメーション( transition
    • アクセシビリティに準拠した表示状態( aria-hidden: false / true;
  • .details-wrapperクリッピング担当レイヤー
    • 直近の親要素 .details によってグリッドアイテム化されている
    • アコーディオンがアニメーション中の詳細部の高さ( grid-template-rows: 0fr 〜 1fr; )の影響を受ける対象
    • 余剰分の非表示( overflow: hidden;
      • ここに同時に余白を付けてしまうと余剰分としては扱われないため、たとえ 0fr でもその値だけ分の空白(margin)・または高さ(padding)を持ってしまう
  • .details-content本体/装飾担当レイヤー
    • アコーディオン詳細部の実質的な本体
      • .details の影響は受けず、グリッドアイテムではないため自由に装飾できる
    • ヘッダー .summary と詳細部 .details の間の余白を指定する
      • 親要素 .details-wrapper余剰分を非表示にするため、 0fr =クローズ時にはここの余白は見えなくなる

つまり「開いた時はヘッダーと詳細部の間に余白を設けた」上で「閉じると中身が見えない」ようにし、「開閉アニメーションさせる」には最低でも3つのレイヤー= <div> が必要になるということです。

詳細部の内側に余白が必要なければ .details-content は厳密には不要ということなのですが、ヘッダー部の下端と詳細部の上端が完全にくっついてしまうため、見栄え上は現実的でない場面が多いです。後からいずれ装飾用に階層を増やさざるを得なくなるくらいであれば事前に考慮しておきましょう。

JS

本当はHTML/CSSだけで完結したい気持ちもあるのですが、こればかりは必須です……。 ただしアニメーションに関わる処理は一切行わず、あくまで開閉に関わる状態の変更のみです。

// アコーディオンの状態(を制御するパラメータ)を切り替える関数
function accordionToggle(acBtn, acDetail) {
	const acBtnExpanded = acBtn.getAttribute('aria-expanded');

	if ( acBtnExpanded === 'false' ) { // アコーディオンがクローズ時
		acBtn.setAttribute('aria-expanded', 'true'); // trueの時「オープン」
		acDetail.setAttribute('aria-hidden', 'false'); // falseの時「オープン」
    // acDetail.setAttribute('inert', ''); でも可
	}

	if ( acBtnExpanded === 'true' ) { // アコーディオンがオープン時
		acBtn.setAttribute('aria-expanded', 'false'); // falseの時「クローズ」
		acDetail.setAttribute('aria-hidden', 'true'); // trueの時「クローズ」
    // acDetail.removeAttribute('inert'); でも可
	}
}

const accordions = document.querySelectorAll('.js-accordion'); //全てのアコーディオン

if ( accordions ) {
	accordions.forEach( (accordion) => {
		const accordionBtn = accordion.querySelector('.js-accordionBtn'); //ヘッダー部
		const accordionDetail = accordion.querySelector('.js-accordionDetail'); //詳細部

		if ( accordionBtn && accordionDetail ) {
			accordionBtn.addEventListener('click', function() { // クリック時イベントを定義
				accordionToggle(accordionBtn, accordionDetail); //ヘッダー部と詳細部を引数で渡す
			});
		}
	});
}

(最低限と言う割には)多く見えてしまうかもしれませんが、クリックでトグル状態を切り替える仕組みとしてこれらだけはどうしても必要かと思います。

まとめ

アクセシビリティを意識している部分を抜きにすれば、そこまでややこしい実装ではないかと思います。ただ、一見実装するだけなら詳細部の <div>1つか2つあれば足りそうなのに、最低でも3つはないと内部の余白を保ったままアニメーションできないというのが厄介な引っかかりどころになります。

序文に反するようですがアコーディオン自体、自作するモーダルとかよりは断然可愛いレベルのコンポーネントなのですが…… <details> に頼れない場面では役立つこともあるかもしれません。

※ちなみに、モーダルを自作する際なども「モーダルの表示範囲」「モーダル内部のクリッピング(スクロール)」「内部の余白」と最低でも3つのレイヤーに分けた方がSafariとかでバグりづらくなります。モーダルの場合は更に「後ろのオーバーレイ」「固定ヘッダー(閉じるボタン)」などが含まれるためより難解な構造になってしまうのですが……。それこそ <dialog> に頼るべきかも。

余談

とにかくアコーディオンのためだけにJSを書く事を忌避する方法として、不可視にした <input type=”checkbox” id="check"><label for="check"> を使ってアコーディオンのトグル状態をHTML/CSSのみで切り替える方法があったりするのですが、殆どの場合これはアクセシビリティ的観点ではかなりバッドな対応になります。隠したチェックボックスをアコーディオン切り替えのためだけに利用しているので、総じてHTML上も意味理由のわからない構造になってしまいます。

そうなるともちろん、用意された <details> <summary> を利用するのが一番セマンティック……なのですが、これもHTMLの意味を重視したコーディングにおいては仕様の壁が大きいようです。

(ヘッダー部であるはずの <summary> の子要素に <p><h1><h6> が指定できない、置いても正しく認識させるためには結局は aria-labelledby が必要になる?など……罠が多い!)

【details/summary】アコーディオンの見出しを“正しい”二段組にする

そもそも「元々アコーディオンでないものをアコーディオン化してほしい」などといった要望があった場合などはおそらく 「ヘッダー部」と「詳細部」は別々に作って並べているはずなので、 <summary><details> に内包されている構造とはスタイル上相容れないことも多いです。

そのため結局JSが必要にはなってしまう・HTML上のシンプルさが落ちるなどのデメリットはありますが、多くのブラウザで最低限のアクセシビリティは保ったまま汎用的なアニメーション対応が可能な方法として紹介させていただきました。

とはいえ、 display: grid;transition の仕様を応用している時点で少しハックっぽい雰囲気は纏った方法なので、さっさとSafariやFirefox辺りが interpolate-size に対応して、それから数年の時が経ってくれることだけを祈ります……🙏 ( ::details-content も「Baseline 2025」になったばかりのようなので、まだ怖いかも……)