先记住这个答案
浏览器做关键帧动画时,会在相邻两个关键帧之间对属性值做插值。opacity 从 1 到 0,中间能算出 0.5,所以是平滑的。但 display: block 和 display: none 之间没有中间状态,浏览器算不出「半个 block」,所以只能在关键帧时间点直接跳变。这就是不可插值属性的处理方式:到了该切的时刻,瞬间换成新值。
- 不可插值属性在关键帧时间点瞬间跳变,没有中间值
display是离散值,不会平滑插值visibility有特殊的离散切换规则allow-discrete可参与离散过渡
display 的跳变时机
很多人以为 display 会在两个关键帧的正中间跳变,其实不是。规范规定:离散属性的值会在前一个关键帧的区间内保持,直到下一个关键帧的时间点才切换。
比如 0% 是 display: block,100% 是 display: none,那整个动画过程中元素都是 block,直到动画结束的最后一刻才变成 none。这个行为其实是有用的——它让你可以在动画期间对元素做 opacity 淡出,动画结束后再用 display: none 彻底移除。
@keyframes fade-out {
from {
opacity: 1;
display: block;
}
to {
opacity: 0;
display: none; /* 动画结束时才跳变 */
}
}
.leaving {
animation: fade-out 300ms forwards;
}opacity 平滑地从 1 过渡到 0,整个过程中 display 一直是 block(因为跳变发生在最后一刻)。动画结束后 forwards 保持最终帧的值,元素变成 display: none,从布局中移除。
visibility 的特殊待遇
visibility 也是离散值,只有 visible 和 hidden 两种状态。但规范对它做了特殊处理:只要起止帧中有一边是 visible,整个过渡期间元素都保持 visible,直到动画结束才切成 hidden。
这个行为和 display 类似,但 visibility: hidden 的元素仍然占据布局空间,display: none 不占。所以淡出动画用哪个取决于你是否需要保留占位。
新规范的 transition-behavior
以前 display 完全不能参与 transition,写 transition: display 300ms 毫无效果。新规范引入了 transition-behavior: allow-discrete,加上之后浏览器就会用和关键帧动画一样的跳变时机来处理过渡。
配合 @starting-style 规则可以给元素第一次出现时设定初始样式,实现「元素从 display: none 变为 block 时淡入」的效果。Chrome 117 和 Safari 17.4 已经支持,Firefox 也已跟进。
容易答错的地方
- 以为所有属性都能做关键帧动画
- 只有可插值属性才能平滑过渡。
font-family、position、display这类属性都是离散的,写进@keyframes不会报错,但只会跳变。面试时说清「可插值」和「离散」的区别就够了。 - 把 display 跳变和 visibility 跳变搞混
display: none会让元素完全脱离文档流,不占空间。visibility: hidden只是视觉上隐藏,空间还在。两者在动画中的跳变时机虽然类似,但对布局的影响完全不同。
面试官还会怎么问?
怎么判断一个 CSS 属性是不是可插值的?
查 MDN 文档,每个属性页面都有 Animation type 字段。写 discrete 的是离散跳变,写 by computed value、length、color 之类的是可插值。没有文档的时候,想一想两个值之间有没有合理的中间态,没有就是离散的。
想让弹窗淡出后从 DOM 流里消失,不用 JavaScript 怎么做?
用 @keyframes 同时写 opacity 和 display。opacity 负责淡出,display: none 写在 to 帧里,动画结束后靠 animation-fill-mode: forwards 保持最终状态。这样元素淡出完毕后就自动不占空间了。
@property 注册的自定义属性能不能让离散值变成可插值?
不能。@property 只能让浏览器知道自定义属性的类型从而进行插值,但前提是那个类型本身得是可插值的。注册成 <length> 或 <color> 就能插值,但没有一个类型能把 block 和 none 变成连续的。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。