CSS アコーディオン アニメーション 9選 — JS最小・CSSだけ
css アコーディオン アニメーション は、クリックした項目だけが高さごと開閉するUI部品を、JavaScriptなしでCSSだけで動かす技術です。この記事では9種類を選び、interpolate-size と calc-size() という2つの仕組みだけで実装しました。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 interpolate-sizeでheight:auto
- 02 name属性で1つだけ開く
- 03 矢印マーカー回転
- 04 本文がフェード+スライド
- 05 開いた項目を強調
- 06 入れ子アコーディオン
- 07 calc-size()フォールバック
- 08 弾性バウンスで開く
- 09 サイドバーメニューに転用
並び順は効果の種類ではなく、実際にアコーディオンを組み立てるときの手順です。まず高さの開閉そのものを動かし(01)、次に一度に1つだけ開く制御を足し(02)、そこへ矢印や本文の登場、強調表示を重ね(03〜05)、最後に入れ子・フォールバック・特別な演出・別の場面への転用という応用を並べました(06〜09)。HTMLとCSSだけでアコーディオンを完結させるため、JavaScriptのファイルは開閉のクラス付け替え以外に入っていません。項目ごとに要点となるコードだけを載せ、完全なファイルはzipの中です。
01interpolate-sizeでheight:auto
「開く」ボタンを押すと、閉じていたパネルが interpolate-size: allow-keywords と calc-size(auto, size) の組み合わせだけで、内容の高さまで滑らかに伸びます。JavaScriptで本文の実際の高さを測る必要がなく、FAQ一覧や規約の詳細表示に向いています。
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(.2, .8, .2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: auto;
}
}
02name属性で1つだけ開く
他の項目を開くと、直前に開いていた項目が自動的に閉じます。ネイティブの <details> 要素にも同じ動きをする name 属性がありますが、このデモでは開閉の速度と曲線を自分で決めたいため、あえてそれを使わずクラスの付け替えで同じ挙動を再現しています。
accs.forEach(function (acc) {
var header = acc.querySelector('.accordion__header');
header.addEventListener('click', function () {
var willOpen = !acc.classList.contains('is-open');
accs.forEach(function (other) {
other.classList.remove('is-open');
other.querySelector('.accordion__header')
.setAttribute('aria-expanded', 'false');
});
if (willOpen) {
acc.classList.add('is-open');
header.setAttribute('aria-expanded', 'true');
}
});
});
03矢印マーカー回転
ヘッダー右側の矢印が、開閉に合わせて45度から-135度まで180度回転し、今どちらの状態かを一目で伝えます。回転だけでなく currentColor を使っているので、見出しの文字色を変えれば矢印の色も自動でそろいます。
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion.is-open .accordion__chevron {
transform: rotate(-135deg);
transition: transform 300ms cubic-bezier(.2, .8, .2, 1);
}
04本文がフェード+スライド
パネルが開くと、本文が上へ8px浮いた位置からフェードインしながら本来の位置に収まります。パネルの高さが伸びる動きより本文の登場を少し遅らせて重ねているため、二段階で開いた印象になります。
.accordion__body {
opacity: 0;
transform: translateY(-8px);
}
.accordion.is-open .accordion__body {
opacity: 1;
transform: translateY(0);
transition:
opacity 200ms ease-out 80ms,
transform 200ms ease-out 80ms;
}
05開いた項目を強調
開いている項目だけ、左側に3px幅の色付き枠と背景色が付き、リストの中でどれが開いているかすぐ分かります。枠線だけを目立たせる設定と、背景色も一緒に変える設定の2通りを選べます。
.accordion {
border-left: 3px solid transparent;
}
.accordion.is-open {
border-left-color: #2f6df6;
background: #fff7e6;
transition: border-color 300ms cubic-bezier(.2, .8, .2, 1);
}
06入れ子アコーディオン
小さな入れ子のアコーディオンが親パネルの中に入っていて、親が開いている間だけ操作できます。親が開いてから少し遅れて子が開き、親が閉じ始める前に子が先に閉じるよう、開始と終了の区間を親より内側にずらしています。
.accordion--inner {
margin: 4px 20px 12px;
background: rgba(255, 255, 255, .08);
border-radius: 12px;
}
.accordion__panel--inner {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion__panel--inner.is-open {
height: calc-size(auto, size);
transition: height 220ms ease-out;
}
07calc-size()フォールバック
最初のブロックは calc-size(auto, size) を使う本来の書き方で、@supports not のブロックが対応していないブラウザ向けの固定高さの代わりです。MDN の calc-size() 対応表 にあるとおり、interpolate-size と calc-size() は2026年9月時点でChrome 129以降だけが対応し、SafariとFirefoxの安定版はまだ対応していません。
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height 300ms cubic-bezier(.2, .8, .2, 1);
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel {
height: 220px;
transition: height 300ms cubic-bezier(.2, .8, .2, 1);
}
}
08弾性バウンスで開く
パネルが開くとき、中の本文がいったん1.06倍まで軽く膨らんでから元の大きさに戻り、弾んだような着地になります。高さの伸び自体は他の項目と同じ曲線なので、弾む動きは本文のスケールだけが担っています。
.accordion.is-open .accordion__body {
animation: bounce-in 340ms cubic-bezier(.2, 1.4, .4, 1) both;
}
@keyframes bounce-in {
0% { transform: scale(1); opacity: 0; }
60% { transform: scale(1.06); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
09サイドバーメニューに転用
同じヘッダーとパネルの組み合わせを、サイドバーメニューの子項目の折りたたみにそのまま転用しました。矢印アイコンと interpolate-size の書き方は01と共通で、違うのは横幅とインデントの数値だけです。
.sidebar__list {
list-style: none;
margin: 0;
padding: 0 18px 10px 32px;
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.sidebar__item.is-open .sidebar__list {
height: calc-size(auto, size);
transition: height 220ms ease-out;
}
どこで崩れるか — 落とし穴
9つとも interpolate-size: allow-keywords と calc-size(auto, size) の2つだけでパネルの高さを動かしていて、JavaScriptで本文の高さを測るコードは1行もありません。1つ目の落とし穴は07のフォールバックで最初に踏んだもので、対応していないブラウザ向けに height: 220px のような固定値を書くと、本文の文字量が増えて実際の高さが変わってもフォールバックの数値だけ古いままになります。
2つ目は06の入れ子アコーディオンを組み立てたときのことです。親のパネルと子のパネルに同じ開閉区間を使ったら、親が閉じ始めても子だけ開いたまま残り、閉じる順番がそろわず二重に閉じているように見えました。子の開始と終了を親より内側の区間へ収め、子が閉じ切ってから親が閉じ始めるよう区間をずらしたところ、この崩れは直りました。修正済みの9ファイルを開くには 34fn5mpm という文字列が必要です。下のカードからzipを取ってください。
3つ目はネイティブの <details> 要素です。name 属性を使えば02のような排他開閉はCSSアニメーションなしでもブラウザ標準で動きますが、MDN の details 要素の対応表 を見ると name 属性が対応しているのはChrome 120以降・Safari 17.2以降・Firefox 130以降で、対応前のブラウザでは属性そのものが無視され、複数の項目が同時に開いたままになります。zipのファイルはすべて <details> ではなくクラスの付け替えで実装しているため、この非対応の影響は受けません。
アクセシビリティ
9つとも prefers-reduced-motion: reduce で animation: none と transition: none になり、開閉の動きが止まります。デモ用のパネルだけは height: auto を残して開いた状態を表示し、情報そのものは隠しません。
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__chevron,
.accordion__body { animation: none !important; transition: none !important; }
.accordion__panel.is-demo { height: auto; }
}
アコーディオン以外のCSSアニメーションは CSSアニメーションのカテゴリー に、このサイトについては 紹介ページ にまとめています。
FAQ
9つを同じページに全部置いても問題ありませんか?
問題ありません。02の排他開閉はグループ単位なので、ページ内に複数のアコーディオングループがあっても、それぞれのグループの中でだけ1つが開きます。06の入れ子だけは親と子で開閉の区間が重なりますが、同じページに何組置いても仕組みは崩れません。
Reactではクラスの付け外しの代わりに何を使いますか?
zipの react/ フォルダの各コンポーネントは、開閉状態を1つのbooleanのstateで持ち、is-open に相当するクラス名をそのstateから付け外ししているだけです。高さの計算とアニメーションのタイミングはこれまでと同じCSSの interpolate-size と calc-size() が担うので、JavaScript側に高さを測るコードはありません。
スマホでも同じように開閉しますか?
はい、タップで同じように開閉します。ただしcalc-size()に対応していないiOSのSafariでは、07のフォールバックが働いて height: auto へ一瞬で切り替わり、なめらかに伸びる動きにはなりません。09のサイドバー転用だけは横幅がスマホの画面幅に合わせてclampで縮むため、矢印とテキストの間隔が詰まります。