はじめに
ヘッダー部をクリックすることで続く詳細部の内容を開閉させることのできるUI、いわゆるアコーディオンは、その見た目や機能から想像するよりは遥かに実装が厄介なUIコンポーネントの一つです。
ただ「開閉する=折りたたむだけ」ならば大した問題はなく、それこそHTMLには <details> と <summary> という折りたたみ要素を作成するためのタグが存在します。以下のように記載するだけで各ブラウザ標準のUIとして簡易な折りたたみ要素=アコーディオンの機能が成立します。
<details>
<summary>ヘッダー部</summary>
ここに詳細が入りますここに詳細が入りますここに詳細が入ります...
</details>
持たせたい機能としてはこれだけで事足りてはいるはずなのですが、「アコーディオン」としてのリッチなUI表現で 「開閉時にアニメーションを付けたい」という要望があれば、それだけでこの <details> 〜 <summary> はほぼほぼ実質使用することができなくなってしまいます。
理由については以下の記事などを参照していただければと思いますが、主に詳細の高さを自動計算する height: auto; とCSSアニメーション transition の相性が悪く、その解決策として新たに用意されたプロパティも、直近では利用できる環境が限られており動作が安定しないためです。
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">
<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-expanded が false の時に「 非 表示かどうか」を表す aria-hidden は true とあべこべになっているのは正直わかりづらくて混乱しがちです……しますよね?)
※わかりやすさならば aria-hidden="true" = hidden 属性として代替も可能かと思いますが、おそらくリセットCSS等で [hidden] { display: none; } が指定されていることも多いかと思うので、置き換える場合はtransitionによるアニメーションに支障が出ないよう注意して @starting-style などの併用も検討してください。
それでは、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-rows を 0fr ⇔ 1fr に transition させることでアニメーションを再現します。
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の時「オープン」
}
if ( acBtnExpanded === 'true' ) { // アコーディオンがオープン時
acBtn.setAttribute('aria-expanded', 'false'); // falseの時「クローズ」
acDetail.setAttribute('aria-hidden', 'true'); // trueの時「クローズ」
}
}
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 が必要になる?など……罠が多い!)
そもそも「元々アコーディオンでないものをアコーディオン化してほしい」などといった要望があった場合などはおそらく 「ヘッダー部」と「詳細部」は別々に作って並べているはずなので、 <summary> が <details> に内包されている構造とはスタイル上相容れないことも多いです。
そのため結局JSが必要にはなってしまう・HTML上のシンプルさが落ちるなどのデメリットはありますが、多くのブラウザで最低限のアクセシビリティは保ったまま汎用的なアニメーション対応が可能な方法として紹介させていただきました。
とはいえ、 display: grid; や transition の仕様を応用している時点で少しハックっぽい雰囲気は纏った方法なので、さっさとSafariやFirefox辺りが interpolate-size に対応して、それから数年の時が経ってくれることだけを祈ります……🙏
( ::details-content も「Baseline 2025」になったばかりのようなので、まだ怖いかも……)
J.B.Goode Inc.に所属しています。良ければフォローお願いします!
J.B.Goode Inc.のウェブサイトでは、技術記事の他にも技術ナレッジや日々の気づき等を配信しています。
https://www.jbgoode.jp/
カジュアル面談も実施中です。お気軽にお問い合わせください。
https://www.jbgoode.jp/recruit/