弹窗打开那一帧动画消失了,你以为是代码写错了?其实是 @starting-style 的 specificity gotcha

写过三年 CSS 弹窗动画,你一定见过这个场景:给对话框写好了淡入效果,加上 transition,打开的一瞬间——动画没了,弹窗直接蹦出来。

这不是你的代码有问题。这是 CSS transitions 的设计限制:它只能从「上一个状态」过渡到「新状态」,但元素第一次出现时,没有「上一个状态」可以过渡。

这就是为什么无数人在 open 的那一帧用 setTimeout(fn, 0) 或者双层 requestAnimationFrame ——硬凑一个延迟,等浏览器先渲染完,再触发过渡。

@starting-style 彻底解决这个问题。Chrome 117+、Safari 17.4+、Firefox 129+ 全部支持,全球覆盖率超过 90%,你可以在生产环境直接用。

它的用法很简单:

.toast { opacity: 1; transform: translateY(0); transition: opacity 250ms ease, transform 250ms ease; }
@starting-style { .toast { opacity: 0; transform: translateY(0.75rem); } }

浏览器会在元素第一次渲染时,把 @starting-style 里的声明当作「上一个状态」,然后从那里开始过渡。零 JavaScript,零延时 hack。

但问题来了。

Josh Comeau 发现的 specificity gotcha

2025 年 9 月,Josh Comeau 在博客里写了一个让很多人后背发凉的现象:

@starting-style 里的声明,不像 @keyframes 那样被提升优先级。

用 @keyframes 写入场动画时,即使你的元素有更高特异性的内联样式,from 状态依然会被尊重,因为 @keyframes 有特殊的优先级提升机制。

但 @starting-style 没有。这意味着:

.popover-content[data-state=”visible”] { opacity: 1; transform: scale(1); }
@starting-style { .popover-content[data-state=”visible”] { opacity: 0; transform: scale(0.9); } }

结果:动画完全不触发,但浏览器没有报错,开发者还以为是自己写法有问题。

三个解决方案

解法一:CSS 变量(优雅但复杂)

把起始值存进 CSS 变量,让 @starting-style 写入变量:

.popover { –enter-opacity: 0; –enter-transform: scale(0.9); opacity: 1; transform: var(–enter-transform); transition: opacity 0.3s ease, transform 0.3s ease; }
@starting-style { .popover { –enter-opacity: 0; –enter-transform: scale(0.9); } }

这样即使高特异性规则覆盖了 opacity 属性,变量里的起点值仍然会被浏览器在计算起始状态时读取到。

解法二:@keyframes(简单且可靠)

这是 Josh Comeau 本人更推荐的做法。@keyframes 不受 specificity 影响,是最稳定的方案:

@keyframes popover-enter { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
.popover { animation: popover-enter 0.3s ease-out forwards; }

缺点是需要单独写一个 @keyframes 块,如果你的弹窗状态很多(toast、dropdown、modal),每个都写一个会增加 CSS 体积。

解法三:!important(不推荐,但有效)

在 @starting-style 里加 !important 可以强行提升优先级,但这会让维护变复杂。

还有一个你可能忽略的限制

@starting-style 只会触发一次——在元素第一次被渲染的时候。同一个元素,先关闭再打开,第二次打开时 @starting-style 不会再次触发。

正确做法是把关闭状态和打开状态都写成「过渡终点」,@starting-style 只负责第一次的冷启动,配合 transition-behavior: allow-discrete 让 display 也参与过渡。

该用哪个?

用 @keyframes:弹窗、modal、toast 这些状态清晰、动画简单的组件,@keyframes 是最省心的选择,不需要担心 specificity。

用 @starting-style:需要 transition 的缓动曲线或者与其他过渡复用同一条 transition 规则时,@starting-style 更合适。

别用 !important:除非你确定这段 CSS 永远不会被覆盖。

所以,@starting-style 并不总是优于 @keyframes。它解决了一个具体的历史遗留问题,但也带来了新的 gotcha。理解这两个工具各自的边界,才是真的把动画这件事用对。

评论区

0 条评论

登录后可评论。