先记住这个答案
CSS 过渡需要一个起始值和一个结束值来做数值插值。auto 是一个关键字,不是具体数值,浏览器拿不到两个数来算中间帧,所以 height: 0 到 height: auto 不会有动画效果,而是直接跳变。解决思路有三种:用 max-height 估上限值、用 Grid 行高做 0fr 到 1fr 的过渡、或等浏览器支持 interpolate-size。
height: auto不是数值,浏览器无法对它做插值max-height方案简单但收起时有明显停顿感- Grid 行轨道可实现部分展开动画
interpolate-size可让尺寸关键字参与插值
max-height 技巧和它的缺陷
最常见的做法是不过渡 height,改成过渡 max-height。收起时设 max-height: 0,展开时设一个比内容实际高度大的值,比如 max-height: 500px。
.panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.panel.open {
max-height: 500px; /* 估一个比内容高的值 */
}这个方案能用,但有明显缺点。展开时,动画从 0 到 500px 的过程中,实际内容可能只有 100px 高。当 max-height 超过内容高度后,内容已经完全显示了,剩余的动画时间在空跑。视觉上就是内容很快就出来了,然后停了一下才算结束。收起时更明显:从 500px 开始往下减,前 400px 的变化用户完全看不到,到了 100px 才开始有视觉变化,给人「顿了一下才动」的感觉。
所以 max-height 的大值不能随便给。设得太大,停顿感重;设得太小,内容会被截断。如果内容高度是动态的,就很难挑到合适的值。
Grid 行高方案:不需要知道具体高度
用 CSS Grid 可以绕开「不知道内容多高」的问题。思路是把要展开的内容放在一个 Grid 容器里,过渡容器的行高从 0fr 到 1fr。
.wrapper {
display: grid;
grid-template-rows: 0fr; /* 收起:行高为 0 */
transition: grid-template-rows 0.3s ease;
}
.wrapper.open {
grid-template-rows: 1fr; /* 展开:行高自适应内容 */
}
.wrapper > .content {
overflow: hidden; /* 收起时隐藏溢出内容 */
}0fr 和 1fr 都是数值,浏览器可以在它们之间插值。1fr 的实际高度等于内容的高度,所以动画的节奏和内容高度完美匹配,不会有 max-height 那种空跑的问题。这是目前兼容性和效果最好的方案,主流浏览器都已支持。
未来的原生方案:interpolate-size
Chrome 129 开始支持 interpolate-size: allow-keywords。加了这个声明后,浏览器会在过渡时先计算出 auto 对应的具体像素值,然后正常插值。这意味着可以直接写 height: 0 到 height: auto 的过渡了。
这个属性可以设在容器上让子元素继承,也可以设在 :root 上全局生效。不过目前 Firefox 和 Safari 还没支持,生产环境建议先用 Grid 方案,把 interpolate-size 作为渐进增强。
容易答错的地方
- 以为用 JS 先读高度再设上去就行
- 用 JS 读取
scrollHeight然后设成height确实可以过渡,但这种做法在内容高度变化时要重新测量,而且读scrollHeight本身会触发强制布局。如果在动画帧里频繁读写,反而引起性能问题。 - 以为 max-height 设得够大就没问题
max-height设得越大,空跑时间越长。比如内容 200px,max-height设 2000px,收起动画 0.3 秒里前 90% 的时间用户完全看不到变化。
面试官还会怎么问?
展开收起时想顺便做 opacity 渐显怎么配合?
在同一个元素上加 transition: grid-template-rows 0.3s, opacity 0.3s,展开时 opacity 从 0 到 1,收起时反过来。opacity 不触发布局,两个过渡并行不影响性能。
为什么不用 scaleY 来模拟展开?
scaleY(0) 到 scaleY(1) 只是视觉上缩放,元素在布局中仍然占据原来的空间。展开面板下面的内容不会被推开,视觉效果和真正的高度变化差别很大。
收起的时候想要不同的缓动曲线怎么做?
在收起状态写 transition: grid-template-rows 0.3s ease-in,在展开状态写 transition: grid-template-rows 0.3s ease-out。浏览器用的是目标状态的 transition 声明,所以展开时读 .open 的缓动,收起时读默认状态的缓动。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。