導入
開閉UIの印象はタイミングで変わる。アコーディオンや details 要素は、瞬時に切り替わるだけでも機能は満たす。だが、高さがなめらかに伸び、中身が少し遅れて現れると、押した操作への反応が見えやすくなる。
この差は、高さと中身を動かすタイミングで作る。同じ時間でまとめて動かさず、別のタイミングと別の長さで動かし、開く時と閉じる時で順序を反転させる。この記事では、個々のCSS機能より、この時間差の組み方を扱う。
先にデモで、開く時と閉じる時の順序、時間差、イージングを整理する。後半では、同じ見せ方をCSSだけで実装する例を扱う。
デモでは、外枠と中身がどの順序で動くかを見る。
以降のコードでは、時間とイージングを次の変数で表す。
:root {
--durations-slower: 500ms; /* 開く時の高さ展開にかける時間 */
--durations-slow: 350ms; /* 閉じる時の高さ収縮にかける時間 */
--durations-fast: 100ms; /* 開く時に中身を遅らせる時間 */
--durations-normal: 200ms; /* 閉じる時に中身を消す時間 */
--easings-ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
タイミング設計
開く時と閉じる時で動きの順序を変える。以下の抜粋では、時間差の核である height / opacity / transform だけを取り上げ、後半で解説する content-visibility の行を省いている。
ここでは transition の所要時間と遅延を見る。時間の値が2つある場合は、1つ目が所要時間、2つ目が遅延である。遅延を省いた行では、遅延は 0s として扱われる。
開く時は、高さに遅延がなくすぐ伸び始める。中身は遅れてフェードインする。
details[open]::details-content {
/* 高さは遅延なしで伸び、中身は --durations-fast だけ遅れて追従する */
transition:
height var(--durations-slower) var(--easings-ease-out-expo),
opacity var(--durations-slower) var(--easings-ease-out-expo) var(--durations-fast),
transform var(--durations-slower) var(--easings-ease-out-expo) var(--durations-fast);
}
閉じる時は逆になる。中身が即座にフェードアウトし、高さは遅れて、開く時より短い時間で収縮する。
details::details-content {
/* 中身は遅延なしで消え、高さは 0.08s 遅れて開く時より短い時間で収縮する */
transition:
height var(--durations-slow) var(--easings-ease-out-expo) 0.08s,
opacity var(--durations-normal),
transform var(--durations-normal);
}
開く時と閉じる時で別の transition を書き分けられるのは、ブラウザがプロパティの変化時に、変化後の状態に適用されている transition の値を使うためである。open 属性が付くと details[open]::details-content 側の指定で開き、外れると基底の details::details-content 側の指定で閉じる。閉じる時の動きを基底セレクタに書いているのは、この仕組みによる。
時間差
重ねて動かし、閉じる側を短くする。変数の値を当てはめて時系列に並べると以下の通りである。

閉じる時の遅延 0.08s は、中身のフェードに使う 200ms より短い。フェードの完了を待たず、途中から高さの収縮が重なって始まる。前の動きが終わるのを待つと間延びするため、重ねて動かす。この 0.08s は式から導いた値ではなく、動かしながら合わせた調整値である。
「外枠(高さ)を先に動かし、中身を後から見せる」順序は、Material Designのchoreographyが示す原則に沿う。複数の要素を一度に動かさず、時間差で見せると視線の移動を追いやすい。開閉で順序を反転させると、開く時は外枠から内側へ、閉じる時は内側から外枠へと視線の流れがつながる。
閉じる側は開く側より短い。中身の退場は 200ms、高さの収縮は 350ms で、開く時の 500ms より速く片付く。画面から去る要素は注意を引く必要が薄いため、短く速く処理した方が操作の待ち時間として意識されにくい。これはMaterial Designのduration and easingが示す、退場側の時間を短くする方針とも一致する。
見え方の調整
中身には小さな移動を足す。この実装では opacity だけでなく transform: translateY(-8px) を併用している。中身は少し上にずれた位置から、スライドしながらフェードインする。
opacity の変化に小さな移動を添えると、入場の唐突さが減る。移動量は 8px と小さくとどめる。距離を大きくすると演出が前に出て、操作への反応から離れていく。
イージング
イージングは止まり方を決める。ここでは曲線の定義より、操作直後にすばやく反応し、終端で減速して柔らかく止まるかを確認する。
開閉のようなUIには ease-out 系が向く。web.devの記事でも、序盤の速い ease-out は反応の良さを感じさせ、終盤の減速は自然に見えると説明されている。逆に ease-in は序盤が遅く、操作への反応が鈍く感じられるためUIには向かない。
この実装では、標準の ease-out より強く減速する cubic-bezier(0.16, 1, 0.3, 1) を使う。easings.net の easeOutExpo に近い曲線で、動きの大半が序盤に集中する。500ms は数値だけ見ると長めだが、終盤の減速を引き延ばすため、体感は数値ほど遅くならない。
実装
ここからはCSSで実装する例である。ここまでのタイミング設計を details 要素に落とし込む。CSSだけで実装する場合は、高さ auto を補間することと、閉じる瞬間に中身を消さないことが課題になる。
サンプルでは、開く時と閉じる時で transition を分ける。高さの伸縮、中身のフェード、移動量、動きを減らす設定も同じ場所にまとめる。
コードは次の通りである。
@supports (interpolate-size: allow-keywords) {
details::details-content {
height: 0;
overflow-y: clip;
opacity: 0;
transform: translateY(-8px);
/* 閉じる時: コンテンツが先にフェードアウト → 高さ収縮 */
transition:
height var(--durations-slow) var(--easings-ease-out-expo) 0.08s,
opacity var(--durations-normal),
transform var(--durations-normal),
content-visibility var(--durations-slow) ease allow-discrete;
}
details[open]::details-content {
height: auto; /* fallback */
height: calc-size(auto, size);
opacity: 1;
transform: translateY(0);
/* 開く時: 高さが先に展開 → コンテンツがフェードイン */
transition:
height var(--durations-slower) var(--easings-ease-out-expo),
opacity var(--durations-slower) var(--easings-ease-out-expo) var(--durations-fast),
transform var(--durations-slower) var(--easings-ease-out-expo) var(--durations-fast),
content-visibility var(--durations-slower) ease allow-discrete;
}
@media (prefers-reduced-motion: reduce) {
details::details-content,
details[open]::details-content {
/* 移動系は即時化し、開く時のフェードだけ残す */
transition: opacity 0.2s;
}
}
}
実装上の注意
transition: all と @keyframes は避ける。この時間差を保つには、実装で避けたい指定が2つある。ひとつは transition: all である。全プロパティが同じ時間と遅延で動き、時間差を作る余地がない。
もうひとつは @keyframes である。開閉は連打されうるが、transition なら途中で反転しても現在の値からなめらかに引き返す。@keyframes はアニメーションを毎回最初から再生する。
このコードでは、CSS機能ごとに次の役割を持たせている。
::details-content で展開部分を直接動かすcalc-size() で外枠を伸縮させるallow-discrete で閉じる時に中身を残す
展開部分
外枠と中身の時間差を作るには、summary ではなく展開部分だけを動かす必要がある。::details-content は summary を除いた展開部分を表すため、通常の details のまま、ラッパー要素なしで中身にスタイルを当てられる。
閉じた状態のスタイルは基底の details::details-content に書き、開いた状態は details[open]::details-content で上書きする。こう分けると、閉じる時と開く時で別の transition を使える。
外枠の伸縮
時間差の設計をCSSだけで表すには、高さを 0 から auto まで直接アニメーションできる必要がある。この例では指定を details の展開部分の height だけに閉じ込めたいので、値の側で指定できる calc-size() を使う。複数の要素やプロパティで内在サイズキーワードを補間したい場合は、interpolate-size: allow-keywords をスコープ指定する方が向く。
details[open]::details-content {
height: auto;
height: calc-size(auto, size);
}
calc-size(auto, size) は auto を計算できるサイズとして扱う記法である。height: auto と height: calc-size(auto, size) を続けて書いているのは、値単位のフォールバックを兼ねる。ただし、このサンプルでは外側の @supports によって、未対応ブラウザには開閉演出全体を適用しない。
高さを補間できるようにしたうえで、閉じる時は中身のはみ出しも抑える必要がある。高さだけを縮めても中身はその場に残り、箱からはみ出して見えてしまう。サンプルコードが閉じた状態に overflow-y: clip を指定しているのはこのためである。clip は hidden と違いスクロールコンテナを作らないため、はみ出しを切り取るだけで意図しないスクロールも発生しない。
閉じる時の表示
閉じる時は、中身をフェードアウトさせる間だけ表示したままにする必要がある。details を閉じると、::details-content にはUAスタイルシートによって content-visibility: hidden が設定される。何もしなければ中身は閉じた瞬間に消え、フェードアウトが見えない。
この切り替えを遅らせるために allow-discrete を使う。content-visibility のように中間状態を持たないプロパティは離散プロパティ(discrete)であり、通常は値が即座に切り替わる。transition-behavior: allow-discrete を指定すると、離散プロパティもトランジションのタイミングに参加できる。
display: none と content-visibility: hidden は例外的に、非表示にする方向ではトランジションの終了時に切り替わる。この例外により、閉じる時もフェードアウトが終わるまで中身が表示され、opacity や transform のアニメーションが見える。サンプルコードの content-visibility var(--durations-slow) ease allow-discrete が閉じる時の指定である。
対応範囲
CSSだけで実装する場合、対応範囲は主に高さの伸縮で決まる。高さの伸縮に必要な calc-size() と interpolate-size は、2026年6月時点でChromium系だけが対応する。未対応ブラウザでも標準の details として内容を開閉できるなら、この実装は追加演出として使える。
対応状況の内訳は details 要素に畳んでおく。
対応状況と実装の詳細
未対応ブラウザには @supports ブロック内のスタイルが適用されない。主なフォールバックはこの分岐であり、height: auto の二重宣言は値単位の保険である。
フェードだけをより広いブラウザに届ける構成もあるが、このCSS例の主題から外れるため扱わない。
アクセシビリティ
開閉アニメーションを入れるなら、動きを減らしたい人の設定を反映する。prefers-reduced-motion: reduce では、高さの伸縮や transform の移動を即時化し、開く時のフェードだけを残す。
@media (prefers-reduced-motion: reduce) {
details::details-content,
details[open]::details-content {
/* 移動系は即時化し、開く時のフェードだけ残す */
transition: opacity 0.2s;
}
}
reduce の目的は、前庭刺激になりやすい移動を減らすことである。閉じる時は高さが即時に畳まれるため、見た目もすぐ閉じる。
すべての変化を即時化したい場合は、transition: none に置き換える。どちらの方式でも開閉の機能は保たれる。
まとめ
開閉の印象は、個々の機能よりタイミングで決まる。開く時は外枠から中身へ、閉じる時は中身から外枠へと順序を反転させ、閉じる側を短くする。こうすると、開閉が押した操作への反応として見えやすい。
今回のCSS実装例では、::details-content で展開部分を直接指定し、calc-size() で高さを補間し、allow-discrete で閉じる時に中身を残す。この組み合わせなら、ラッパー要素なしでも設計した順序を実装できる。
このCSS実装例は、Chromium系向けのprogressive enhancementとして扱う。未対応環境では標準の details の開閉を保つ。見せ方自体は、必要ならJSなど別の手段でも実装できる。
なお、この演出は開閉がときどき起きる程度の操作だから成り立つ。コマンドパレットの開閉のように毎日100回以上繰り返す操作では、アニメーションそのものが遅延になり、付けない判断が妥当になることもある。
参考