CSSで、アコーディオンをなめらかに開閉する方法
<details> で作ったアコーディオンは、中身を包む ::details-content 疑似要素の高さを、0 から auto へ transition で動かすと、なめらかに開閉します。auto へのアニメーションを許す interpolate-size: allow-keywords と、閉じるときに中身がすぐ消えないようにする content-visibility の transition を足せば、JavaScript は要りません。<details> を使わない作りなら、grid-template-rows を 0fr から 1fr へ動かす方法があります。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開いてクリックし、描画のコマごとに要素の高さを測りました。ブラウザの対応バージョンは、@mdn/browser-compat-data 8.1.8 の値です。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げ、ブラウザのページ内検索で閉じた中身が見つかるかどうかは試していません。

details の中身の高さを、0 から auto へ動かす
HTML は、<details> と <summary> をそのまま並べます。summary をクリックしたときに開閉する仕組みは、ブラウザが受け持ちます。
<div class="faq">
<details>
<summary>ごはんは1日に何回ですか</summary>
<p class="body">朝と夜の2回が目安です。量はフードの袋の表示を見て決めます。<a href="#">ごはんの記録</a></p>
</details>
<details>
<summary>爪切りはいつしますか</summary>
<p class="body">2週間に1回が目安です。</p>
</details>
</div>
CSS の全体は次のとおりです。三角の代わりに ::after で矢印を描き、開いたときに回しています。
/* details と summary で作る、なめらかに開閉するアコーディオン */
.faq details {
border-bottom: 1px solid #c9dfe5;
/* auto への補間を、この中だけで許す */
interpolate-size: allow-keywords;
}
.faq summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 8px;
list-style: none;
cursor: pointer;
font-weight: bold;
}
.faq summary::-webkit-details-marker {
display: none;
}
.faq summary::after {
content: '';
width: 8px;
height: 8px;
border-right: 2px solid #067a98;
border-bottom: 2px solid #067a98;
transform: rotate(45deg);
transition: transform 300ms;
}
.faq details[open] > summary::after {
transform: rotate(-135deg);
}
/* 開閉で動かすのは、中身を包む ::details-content の高さ */
.faq details::details-content {
block-size: 0;
overflow: hidden;
transition:
block-size 300ms ease,
content-visibility 300ms allow-discrete;
}
.faq details[open]::details-content {
block-size: auto;
}
/* 余白は ::details-content ではなく、中の要素に付ける */
.faq .body {
margin: 0;
padding: 0 8px 12px;
}
@media (prefers-reduced-motion: reduce) {
.faq details::details-content,
.faq summary::after {
transition: none;
}
}
::details-content は、<details> の中で <summary> 以外の、開閉する部分を指す疑似要素です。閉じているときは高さ 0、開いているときは auto にして、そのあいだを transition でつなぎます。
ただし、auto のような中身で決まる大きさへは、既定ではアニメーションしません。MDN の interpolate-size のページによると、既定の値は numeric-only で、allow-keywords にすると 0 のような長さと auto のあいだを補間できるようになります。この値は子孫に受け継がれるので、:root に書けばページ全体に効きます。ここでは、アコーディオンの details の中だけに効くように書きました。
最初の項目をクリックしてから、details の高さを描画のコマごとに測りました。次の 50ms ごとの値は、その時刻を過ぎて最初に描画されたコマの値です。コマはおよそ 16ms ごとなので、実行のたびに数 px ずれます。
[details の基本形] 閉じた状態 49px
開く: 0ms 49px / 50ms 70px / 100ms 89px / 150ms 100px / 200ms 106px / 250ms 109px / 300ms 109px / 350ms 109px
閉じる: 0ms 109px / 50ms 88px / 100ms 69px / 150ms 58px / 200ms 52px / 250ms 49px / 300ms 49px / 350ms 49px
開くときも閉じるときも、およそ 300ms かけて高さが変わりました。interpolate-size を既定の numeric-only に戻すと、クリックした次のコマで、もう開ききっていました。

閉じるときは、content-visibility にも transition を付ける
transition に content-visibility 300ms allow-discrete を入れているのは、閉じるアニメーションのためです。block-size だけに transition を付けた版も測ると、開くときは同じように動きましたが、閉じるときはクリックした直後から閉じた高さになりました。
[content-visibility の transition を外す] 閉じた状態 49px
開く: 0ms 49px / 50ms 62px / 100ms 84px / 150ms 97px / 200ms 104px / 250ms 108px / 300ms 109px / 350ms 109px
閉じる: 0ms 49px / 50ms 49px / 100ms 49px / 150ms 49px / 200ms 49px / 250ms 49px / 300ms 49px / 350ms 49px
閉じた details では、::details-content の content-visibility が hidden になります。MDN の content-visibility のページによると、hidden の要素は中身の描画を省きます。閉じている途中の値を読むと、transition がない版では 50ms の時点でもう hidden でした。付けた版では、150ms の時点でも visible のままで、400ms の時点で hidden になっていました。
[details の基本形、::details-content の content-visibility] 閉じる: 50ms visible / 150ms visible / 400ms hidden
[content-visibility の transition を外す] 閉じる: 50ms hidden / 150ms hidden / 400ms hidden
同じページにあるとおり、visible から hidden へのアニメーションでは、値が hidden に変わるのは最後の時点です。ただし、transition でこの動きにするには、transition-behavior の allow-discrete が要ります。allow-discrete を付けないと、content-visibility のような離散的なプロパティには transition が始まりません。
余白は ::details-content ではなく、中の要素に付ける
中身の余白を ::details-content に付けると、閉じた状態でも余白の分だけ高さが残ります。.body の padding を外し、同じ padding: 0 8px 12px を ::details-content に付けて測りました。
[余白を ::details-content に付ける] 閉じた状態 61px
開く: 0ms 61px / 50ms 72px / 100ms 89px / 150ms 100px / 200ms 105px / 250ms 108px / 300ms 109px / 350ms 109px
閉じる: 0ms 109px / 50ms 92px / 100ms 77px / 150ms 68px / 200ms 63px / 250ms 61px / 300ms 61px / 350ms 61px
閉じた高さが 49px ではなく 61px になり、block-size: 0 にしても下の余白 12px が残りました。余白は、上の CSS の .body のように、::details-content の中の要素に付けます。
calc-size(auto, size) でも同じように動く
interpolate-size を使わずに、開いたときの高さを calc-size(auto, size) と書く方法もあります。interpolate-size を numeric-only に戻したうえで、次の指定に差し替えて測りました。
.faq details[open]::details-content {
block-size: calc-size(auto, size);
}
[calc-size(auto, size) で書く] 閉じた状態 49px
開く: 0ms 49px / 50ms 62px / 100ms 84px / 150ms 97px / 200ms 104px / 250ms 108px / 300ms 109px / 350ms 109px
閉じる: 0ms 109px / 50ms 88px / 100ms 69px / 150ms 58px / 200ms 52px / 250ms 49px / 300ms 49px / 350ms 49px
開くときも閉じるときも、interpolate-size の場合と同じく 300ms ほどかけて動きました。MDN の calc-size() のページでは、auto への補間を許したいだけなら interpolate-size を使い、calc-size() は計算もしたいときに使うよう勧めています。
動きを減らす設定では、transition を外す
OS の設定で、画面の動きを減らすよう指定している人もいます。その場合は、開閉を動かさないようにします。MDN の prefers-reduced-motion のページにあるとおり、この設定は @media (prefers-reduced-motion: reduce) で受け取れます。上の CSS では、このときに transition: none にしています。Playwright で reducedMotion: 'reduce' を指定して測ると、クリックした直後のコマで開ききり、閉じるときも同じく一度に閉じました。
[details、prefers-reduced-motion: reduce] 閉じた状態 49px
開く: 0ms 109px / 50ms 109px / 100ms 109px / 150ms 109px / 200ms 109px / 250ms 109px / 300ms 109px / 350ms 109px
閉じる: 0ms 49px / 50ms 49px / 100ms 49px / 150ms 49px / 200ms 49px / 250ms 49px / 300ms 49px / 350ms 49px
details を使わないなら、grid-template-rows を 0fr から 1fr へ動かす
ボタンと aria-expanded で開閉を作る場合は、::details-content が使えません。その場合は、中身を包む要素を1行だけのグリッドにして、行の高さを 0fr から 1fr へ動かします。下の CSS には interpolate-size を書いていませんが、高さは少しずつ変わりました。
<button class="trigger" type="button" aria-expanded="false" aria-controls="p1">ごはんは1日に何回ですか</button>
<div class="panel" id="p1">
<div class="inner">
<p class="body">朝と夜の2回が目安です。量はフードの袋の表示を見て決めます。<a href="#">ごはんの記録</a></p>
</div>
</div>
/* details を使わない場合:grid-template-rows を 0fr から 1fr へ動かす */
.panel {
display: grid;
grid-template-rows: 0fr;
visibility: hidden;
transition:
grid-template-rows 300ms ease,
visibility 300ms;
}
.trigger[aria-expanded='true'] + .panel {
grid-template-rows: 1fr;
visibility: visible;
}
/* 1fr の行に入る中身は、min-height: 0 と overflow: hidden で縮める */
.panel > .inner {
min-height: 0;
overflow: hidden;
}
.panel .body {
margin: 0;
padding: 0 8px 12px;
}
@media (prefers-reduced-motion: reduce) {
.panel {
transition: none;
}
}
// ボタンを押すたびに aria-expanded を切り替える
document.querySelectorAll('.trigger').forEach((button) => {
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!open));
});
});
ボタンを押してから、.panel の高さを測りました。
[grid-template-rows の基本形] 閉じた状態 0px
開く: 0ms 0px / 50ms 21px / 100ms 40px / 150ms 51px / 200ms 57px / 250ms 60px / 300ms 60px / 350ms 60px
閉じる: 0ms 60px / 50ms 39px / 100ms 20px / 150ms 9px / 200ms 3px / 250ms 0px / 300ms 0px / 350ms 0px
details の場合と違って、閉じた中身を隠す仕組みは自分で用意する必要があります。ここでは visibility を使いました。閉じた状態で、見出しの summary やボタンにフォーカスして Tab を押し、次にフォーカスが移る要素を調べました。
[details、閉じた状態] Tab のあと: <summary> 爪切りはいつしますか
[grid、閉じた状態(visibility あり)] Tab のあと: <button> 後ろのボタン
[grid、閉じた状態(visibility を外す)] Tab のあと: <a> ごはんの記録
visibility: hidden を外した版では、高さ 0 で見えていないパネルの中のリンクに、フォーカスが移りました。details は、閉じた中身のリンクを飛ばして、次の summary に移っています。MDN の visibility のページにあるとおり、visibility: hidden の要素はフォーカスを受け取れません。
visibility の transition には allow-discrete を付けていません。MDN の同じページによると、visibility は、片方の端が visible なら、transition が終わるまでのあいだ visible として扱われます。閉じている途中の値を読むと、150ms の時点では visible で、閉じ終わった 400ms の時点で hidden になっていました。
[grid、.panel の visibility] 閉じる: 50ms visible / 150ms visible / 400ms hidden
対応しているブラウザ
@mdn/browser-compat-data 8.1.8 で、使ったプロパティが最初に使えるようになったバージョンを調べました。
| 機能 | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
::details-content |
131 | 131 | 143 | 18.4 |
interpolate-size |
129 | 129 | なし | なし |
calc-size() |
129 | 129 | なし | なし |
transition-behavior |
117 | 117 | 129(一部) | 17.4 |
grid-template-rows のアニメーション |
107 | 107 | 66 | 16 |
details の作り方で auto への補間に使う interpolate-size と calc-size() に対応しているのは、表の4つのブラウザのうち Chrome と Edge だけです。Firefox の transition-behavior は、互換性データでは一部だけの対応とされています。MDN でも interpolate-size は実験的な機能とされています。Firefox と Safari でも高さを動かしたいときは、grid-template-rows の作り方を選びます。





