弹窗打开那一帧动画消失了,你以为是代码写错了?其实是 @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。理解这两个工具各自的边界,才是真的把动画这件事用对。
评论区
登录后可评论。