interpolate-size 实战:让 height:auto 真正参与过渡的完整教程

2026-09-25 0 121

做前端这些年,被问得最多的一类问题就是:”为什么我给 height 加了 transition,但它没动?”

第一次遇到这个问题大概是写一个折叠面板。按钮点开,内容从隐藏变显示,想加个过渡让它滑出来。写好了 transition: height .3s,然后把高度从 0 改成 auto——结果”啪”一下就展开了,完全不理会那个 0.3 秒。

当时查到的答案是”CSS 没法对 auto 做过渡,因为浏览器不知道 auto 到底是多少像素”。这个回答没错,但它是个死结:你不设 auto,内容会截断;你设 auto,过渡就不能用。于是这些年大家绕来绕去,写出了一堆 hack。

Chrome 129 稳定的 interpolate-size 和 calc-size(),终于把这个问题从根上解决了。这篇文章把旧方案和新方案一起过一遍,顺带整理了我在实际项目里踩过的几个坑。

一、先讲清楚为什么 auto 不能过渡

CSS 过渡的本质是在两个数值之间做插值。从 100px 到 200px,中间取 150px,这是线性插值,浏览器很擅长。

问题出在 auto 上。auto 不是一个长度值,它是一个尺寸关键字。在布局完成之前,浏览器也不知道它会等于多少像素。而且更麻烦的是,它依赖内容——你把内容删掉一行,auto 就变了;你改一下字号,auto 又变了。

在过渡这件事上,”不确定”和”可以插值”是互斥的。所以规范干脆规定:包含尺寸关键字的两个值之间不能插值。

同类关键字还有 min-content、max-content、fit-content、stretch、contain。它们都不能参与过渡,不只是 auto。

二、过去四种绕法,以及它们各自的代价

在讲新方案之前,先把老办法列一遍。知道它们为什么不够好,才能判断什么时候该换掉。

绕法一:max-height 拉一个大数

.panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s ease;
}
.panel.open {
  max-height: 500px;  /* 随便写一个大数 */
}

这是最流行的做法,也是最烂的一个。问题有两个。第一,动画速度会被”虚高的 max-height”拖慢。内容实际只有 80px 高,你设了 500px,那过渡曲线跑 300ms 的时候,视觉上高度早就到位了,剩下的时间全在等一个看不见的空档。手感特别粘。

第二,你永远不知道写多少才够。写小了内容被截,写大了动画发虚。文本一旦换成可编辑的、或者从接口返回的,这个数就彻底失控了。

我见过最狠的一次,一个产品详情页的 max-height 写成了 9999px,用户点开的时候整个页面要等两秒才开始”动起来”。

绕法二:grid-template-rows 从 0fr 到 1fr

.panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s ease;
}
.panel.open {
  grid-template-rows: 1fr;
}
.panel > .inner {
  overflow: hidden;
}

这个方案比 max-height 聪明得多,因为它真的会把动画时长按照内容实际高度分配,速度是正常的。

代价在于结构。你必须多加一层 div,并且它要带 overflow: hidden。这个 overflow: hidden 在很多场景下会坏事——里面的 position: sticky 会失灵,下拉菜单会被裁掉,阴影也会消失。

另外,如果面板本身就在一个 grid 或 flex 容器里,你再给它套一层 grid,样式调试会变得很费劲。

绕法三:JS 量高度再写成像素

const el = document.querySelector('.panel');
const inner = el.firstElementChild;

el.style.height = '0px';
requestAnimationFrame(() => {
  el.style.height = inner.scrollHeight + 'px';
});
el.addEventListener('transitionend', () => {
  el.style.height = 'auto';
});

这是最”精确”的方案,也是我当年用得最多的。它每次都能跑对,速度也正常。

但它是 JS 驱动的动画,问题在渲染层面。设置 height 会触发布局(layout),而布局是主线程上的事。页面一旦有并发任务,比如在解析大 JSON,这个过渡就会卡顿。

还有一个隐性成本:每个折叠面板都要单独写这段代码,或者封装成一个指令、一个 Hook。写多了就是负担。

绕法四:用 transform: scaleY 假装一下

.panel {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform .3s ease;
}

这个方案有个不可救的缺点:它只是把内容沿垂直方向压扁了,没有真正改变布局占位。文字会跟着变形,图片会被拉长。除非内容是纯色块,否则一眼就能看出问题。

它唯一的优势是走合成层,性能最好。所以有些纯装饰性的背景元素、条状进度条之类,用它没问题。但对折叠面板这种场景,不合适。

三、新方案的第一层:interpolate-size

前面啰嗦这么多,是因为新方案的价值只有跟这些代价对比才能看出来。

在根元素上加一行:

:root {
  interpolate-size: allow-keywords;
}

然后所有原来的写法,一个字都不用改,直接生效:

.panel {
  height: 0;
  overflow: hidden;
  transition: height .3s ease;
}
.panel.open {
  height: auto;
}

就这么简单。浏览器现在知道怎么在 0 和 auto 之间插值了。

它背后的机制是:浏览器先按常规布局算一遍目标状态,拿到 auto 实际对应的像素值,然后把这个值当作插值终点。动画结束之后,计算样式里记录的仍然是 auto,所以内容继续变化时高度依然会跟着走。

为什么要放在根元素上

interpolate-size 本身是可继承的。放在 :root 上,整个文档都打开这个行为。你也可以只在某个容器上开:

.dashboard .card {
  interpolate-size: allow-keywords;
}

为什么有人会选择局部开启?因为这个属性打开的是一整套行为,不只是 auto。min-content、max-content、fit-content 这些关键字也会跟着变成可插值的。如果你的项目里对布局稳定性比较敏感,担心全局打开带来意料之外的变化,可以先用局部开启试一段时间。

不过就我的经验,全局打开之后没出过什么问题,反而省了很多逐处开启的麻烦。

顺带可以插值的几种用法

/* 从 fit-content 到 max-content */
.tag-list {
  width: fit-content;
  transition: width .25s ease;
}
.tag-list:hover {
  width: max-content;
}

/* 从 min-content 到 100% */
.side-note {
  width: min-content;
  transition: width .4s ease;
}
.side-note.wide {
  width: 100%;
}

以前这些场景都要写 JS,现在纯 CSS 就能搞定。

四、新方案的第二层:calc-size()

interpolate-size 解决的是”从 auto 到某个具体值”,但还有一种需求:它不用变到 auto,也不用变成具体像素,而是想要在 auto 的基础上做计算。

比如”折叠后显示 60 像素高,展开后显示完整内容”。折叠状态的 60 像素是固定的,展开状态是 auto。这个用 interpolate-size 就能写。但如果你想写”折叠状态显示完整高度的 30%”,那就得靠 calc-size() 了。

语法长什么样

calc-size(<calc-size-basis>, <calculation>)

第一个参数是基准值,通常是 auto、min-content 这类关键字。calc-size() 会把它先解析成一个实际尺寸,然后在第二个参数里以 size 这个名字引用它。

.preview {
  height: calc-size(auto, size * 0.4);
  transition: height .3s ease;
}
.preview.expanded {
  height: auto;
}

展开的时候,从 auto * 0.4 过渡到 auto,视觉上就是慢慢长高的效果。

在 calc-size 里面还能用别的值

/* auto 高度加 40 像素 */
height: calc-size(auto, size + 40px);

/* auto 高度的一半,但至少 200 像素 */
height: calc-size(auto, max(size / 2, 200px));

/* 取 auto 和视口高度的较小值 */
height: calc-size(auto, min(size, 60vh));

这些写法在以前是不可想象的。你想做”最大展示到视口 60%”,要么用 JS,要么用 max-height: 60vh 加上 height: auto 硬拼——但那样又没有过渡效果。

一个容易看漏的细节

calc-size() 本身是合法的尺寸值,所以它可以出现在任何需要尺寸的地方,不只是 height。比如 padding-block、margin、gap、grid-template-columns 里都可以用。这算是意外之喜。

.list {
  display: flex;
  flex-direction: column;
  gap: calc-size(auto, size * 0.5);
}

不过说实话,这种写法可读性一般,实际项目里很少这么用。知道它能用就行。

五、案例一:自动展开的详情卡片

来写一个完整的东西。一个订单详情卡片,默认只显示摘要,点击标题展开完整信息。

<div class="detail-card">
  <button class="toggle" aria-expanded="false">
    订单 #20250317001
  </button>
  <div class="body">
    <dl>
      <dt>下单时间</dt><dd>2025-03-17 14:22</dd>
      <dt>收货地址</dt><dd>杭州市西湖区……</dd>
      <dt>商品明细</dt><dd>……</dd>
    </dl>
  </div>
</div>
.body {
  height: 0;
  overflow: hidden;
  transition: height .35s cubic-bezier(.4, 0, .2, 1);
}

.detail-card.open .body {
  height: auto;
}

加上前面那一行根元素设置,即可。

document.querySelectorAll('.toggle').forEach((btn) => {
  btn.addEventListener('click', () => {
    const card = btn.closest('.detail-card');
    const open = card.classList.toggle('open');
    btn.setAttribute('aria-expanded', String(open));
  });
});

这段 JS 只负责切换类名,动画完全交给 CSS。和之前的方案比,代码少了:不用在每次渲染后重新测量高度,不用处理 window resize 导致的高度重算,也不用担心图片加载完成后 scrollHeight 变了。

这一点特别值得强调。当时的 JS 方案里,如果详情里有图片,第一次展开时图片还没加载,scrollHeight 量出来是小的;图片加载完之后,实际内容比那个高度高,overflow: hidden 就把下面截掉了。要绕开这个得监听 img.onload 再重算,很烦。

新方案里这个问题根本不存在,因为浏览器每次布局都是最新的。

加一点展开时的淡入

.body {
  height: 0;
  overflow: hidden;
  opacity: 0;
  transition:
    height .35s cubic-bezier(.4, 0, .2, 1),
    opacity .2s ease .08s;
}

.detail-card.open .body {
  height: auto;
  opacity: 1;
}

注意 opacity 的延迟用的是 0.08 秒,让高度先动起来,内容再跟上。这个顺序上的微调能让动画自然很多,比两个一起同步动看起来舒服。

六、案例二:随内容增高的输入框

接着来看一个和打字体验相关的场景。

原生的 <textarea> 尺寸是固定的,用户打满了也不会自己长高,得手动拖那个右下角的小三角。为了绕过这个问题,以前社区里有一堆”自动增高 textarea”的库,原理都是监听 input 事件,用 scrollHeight 反推高度。

CSS 现在有两个特性可以配合起来解决它:field-sizing: content 负责”跟着内容长高”,interpolate-size 负责”长高这件事有动画”。

textarea {
  field-sizing: content;
  min-height: 4em;
  max-height: 20em;
  resize: none;
  overflow-y: auto;
  interpolate-size: allow-keywords;
  transition: height .2s ease;
}

field-sizing: content 让表单控件的尺寸跟着内容走。min-height 和 max-height 给了一个合理区间,超过最大值之后内部出现滚动条。

但这个写法有个细节需要实测确认。因为 field-sizing: content 下,浏览器计算高度的时机和输入法合成的时机有关系。中文输入法在拼音阶段是不会触发高度变化的,只有候选词确认、字符真正落到文本框里,高度才会增长。这个行为其实是好的,因为在拼音阶段高度就跳动会让人没法专注。

英文输入下,每次按键都会触发高度计算。实测在 Chrome 里 20 个字符以内的短内容很顺滑,长内容偶尔会有一帧的滞后,但不明显。

这个特性还没在所有浏览器铺开,field-sizing 的支持面比 interpolate-size 还要窄一些,具体上线前实测一下最稳。

七、案例三:可伸展的侧边栏

最后一个案例稍微复杂一点。一个后台页面,左侧有一个筛选面板,桌面端显示完整的筛选条件,移动端只显示已选条件的摘要,点击之后展开。

摘要显示的内容是动态的——选了两个条件就两行,选了五个就五行。所以高度是个不确定值。

.filter-panel {
  height: calc-size(auto, min(size, 48px));
  overflow: hidden;
  transition: height .3s ease;
}

.filter-panel.open {
  height: auto;
}

这里的 calc-size(auto, min(size, 48px)) 意思是:以内容完整高度为基准,取它和 48 像素里较小的那个。所以内容不足 48 像素时,面板就跟着内容的高度;内容超过 48 像素时,面板就停在 48 像素,超出部分被 overflow: hidden 裁掉。

这个写法解决了以前必须硬编码一个具体数字的问题。摘要行数会变,但对折叠状态来说,只要不超过 48 像素就都以内容为准,超过就统一 48 像素,行为是稳定的。

展开的时候从 48 像素过渡到完整高度,速度也是正常的,不会因为内容多少而忽快忽慢。

和容器查询配合

.filter-panel {
  container-type: inline-size;
}

.filter-panel .wide-only {
  display: none;
}

@container (min-width: 400px) {
  .filter-panel .wide-only {
    display: block;
  }
  .filter-panel {
    height: auto;
  }
}

这样在宽容器里就直接展开,不参与动画。窄容器里才走折叠逻辑。这种响应式和动画的配合在以前是很难做干净的,因为 JS 方案里一旦切换断点,就得重新测量、重新绑定。

八、五个实际踩过的坑

坑一:给 height 写 transition 时容易忘记 overflow

/* 高度过渡到 0,但里面的内容会把容器撑开 */
.panel {
  height: 0;
  transition: height .3s ease;
}

这个失误看起来低级,但实际上很常见。因为 height: 0 的时候,如果内容超出,浏览器默认的 overflow: visible 会让内容显示在盒子外面。视觉上就是”折叠了但没折”。

解决办法就是在折叠容器上加 overflow: hidden。这一条和过渡没关系,只是基本布局规则,但在新方案下也一样适用。

坑二:过渡过程中改变内容会中断动画

.card.open .body {
  height: auto;
}

展开动画正跑到一半的时候,如果同时有一个接口返回,往 .body 里插了几行内容,会发生什么?

浏览器会重新算布局,auto 对应的目标值变了,动画的终点也就跟着变。实际表现是动画会突然”跳”一下,然后按新的终点继续跑完。

这个行为在大多数场景下是可以接受的,但如果你的产品对动画连贯性要求很高,做法是让数据在动画开始之前就准备好。或者把加载状态放到折叠区域外面的一个固定高度的骨架里。

坑三:性能上它仍然触发布局

interpolate-size 改变的只是”能不能插值”,没有改变”高度变化会触发布局”这件事。每一帧动画都要重排布局,这是主线程上的工作。

在一个页面上同时跑四五个高度动画,或者动画元素内部有很复杂的树时,可能会出现卡顿。这种情况下可以把动画放缓一点(比如从 .2s 改成 .3s),或者干脆用 @media (prefers-reduced-motion) 关掉动画。

@media (prefers-reduced-motion: reduce) {
  .panel,
  .body {
    transition: none;
  }
}

这段代码应该成为项目里的标配,不只是为了动画性能,也是无障碍的基本要求。

坑四:和 will-change 一起用要小心

有一些老文章建议给做动画的元素加 will-change: height,说是能提升性能。但对 height 这种触发布局的属性,will-change 的作用有限,反而会让浏览器长期为一个很少改变的属性保留优化资源,结果是内存占用增加、整体性能下降。

我的建议是:不要给 height、width 这类布局属性加 will-change。留给 transform 和 opacity 就好。

坑五:旧版浏览器的降级表现

目前(截至撰稿时)这个特性在 Chrome、Edge、Safari 上都已经支持得不错,Firefox 还在跟进。interpolate-size 不支持的时候,浏览器会忽略这行声明,那过渡就会”瞬跳”——从 0 直接跳到 auto,没有中间过程。

对可用性来说,这不算致命问题,功能是好的,只是没有动画。如果你确实希望在不支持的浏览器里也有动画,可以用 @supports 检测,加一个 max-height 的兜底:

.body {
  height: 0;
  overflow: hidden;
  transition: height .35s ease;
}

.detail-card.open .body {
  height: auto;
}

@supports not (interpolate-size: allow-keywords) {
  .body {
    height: auto;
    max-height: 0;
    transition: max-height .35s ease;
  }
  .detail-card.open .body {
    max-height: 600px;
  }
}

这个写法我一般不太推荐,因为它把两套逻辑混在一起,维护起来累。更好的做法是接受”新浏览器有动画,旧浏览器没动画”,把系统复杂度降下来。毕竟现在用旧浏览器的人越来越少,为了那一小部分体验而付出维护成本,通常不划算。

九、什么时候不该用

这个特性也不是万能的,有三种场景我建议换别的方案。

第一种是纯装饰性的高度变化,比如进度条填充、分隔线展开。这种情况下用 transform: scaleY 更好,因为它走合成层,不触发主线程布局。视觉效果也够用,因为这类元素通常没有文字内容会被拉变形。

第二种是内容非常庞大的折叠区域。如果一个折叠面板里面是几百个 DOM 节点,每次展开都要重新布局整棵子树,动画是会卡的。这种场景更好的方案是虚拟滚动,只渲染可见区域。

第三种是需要精确控制动画曲线的场景。比如产品要求”先快后慢并且中间有一小段回弹”,光靠 height 的 transition 不好表达,这种情况用 Web Animations API 会更顺手。

除此之外,折叠面板、下拉菜单、展开详情、自适应输入框这些场景,都可以放心用新方案替换掉老代码。

十、写在最后

回过头看这个问题,会发现它其实是一个典型的”平台能力缺失导致的架构扭曲”。因为 CSS 不能对 auto 做过渡,这些年社区被迫发明了至少四种 walk-around,每一种都有自己的坑和成本。这些坑被写进了各种教程,变成了很多人的肌肉记忆,以至于新方案出来之后,有人第一反应是”这不就是我原来写的?”

实际上差别很大。max-height 那个大数、grid 那层多余的 overflow: hidden、JS 那套测量加回写、以及每一个都逃不掉的 resize 监听,全都不需要了。代码从三十行变成三行,还顺手解决了图片加载导致的高度错位问题。

如果你手上正好有个项目在维护折叠面板,可以挑一个不忙的下午,把 :root { interpolate-size: allow-keywords; } 加进去,试着删掉那些 hack。删的过程中你会发现,之前的代码比你以为的还要绕。

interpolate-size 实战:让 height:auto 真正参与过渡的完整教程
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css interpolate-size 实战:让 height:auto 真正参与过渡的完整教程 https://www.taomawang.com/web/css/2818.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务