写过 CSS 入场动画的人都踩过这个坑——元素从 display: none 冒出来那一帧,要么没有过渡直接蹦出来,要么要写两段 JS 监听器才能让它「记住」自己从哪开始。今天 CSS 用两行 @starting-style 把这件事彻底原生化了,连 JS 都不用写。

写过 CSS 入场动画的人都踩过这个坑——元素从 display: none 冒出来那一帧,要么没有过渡直接蹦出来,要么要写两段 JS 监听器才能让它「记住」自己从哪开始。今天 CSS 用两行 @starting-style 把这件事彻底原生化了,连 JS 都不用写。

这个坑的本质是什么

CSS 过渡需要两个状态:起始值和终值。元素从无到有时,浏览器找不到「之前」的状态——它不知道你希望元素从 opacity: 0 还是 opacity: 0.3 开始,所以直接跳到终态,过渡效果自然就消失了。

以前的解法靠 JS:先加类让元素变成起始样式,浏览器渲染完下一帧,再加另一个类切到终态。这段「等一帧」的 JS 是所有 entry 动画抖动或失效的根源。

@starting-style 的解法

@starting-style 的作用就是告诉浏览器:「在这个元素第一次被渲染之前,它应该长这个样子」。浏览器会在元素进入视口的第一帧,自动把它套进 @starting-style 里定义的样式作为起始状态,然后立刻过渡到正常样式——整个过程零 JS。

举一个 dialog 的完整写法:

.dialog {
  opacity: 1;
  scale: 1;
  transition: opacity 0.2s ease-out, scale 0.2s ease-out,
              display 0.2s ease-out allow-discrete,
              overlay 0.2s ease-out allow-discrete;
}

@starting-style {
  .dialog {
    opacity: 0;
    scale: 0.95;
  }
}

.dialog[hidden] {
  opacity: 0;
  scale: 0.95;
}

HTML 只需要一个 button 和一个 dialog:

<button popovertarget="my-dialog">打开</button>
<dialog id="my-dialog" popover>
  <p>Hello, 零 JS 入场动画</p>
</dialog>

点击按钮,对话框从 opacity: 0 / scale: 0.95 过渡到 opacity: 1 / scale: 1,全程浏览器自己算时机,dialog 元素自带焦点管理和 Escape 关闭,都不需要 JS 管。

popover 配合更干净

如果不用 dialog 而是用普通 div + popover 属性,@starting-style 的效果也一样,只是省掉了浏览器内置的焦点管理——需要自己处理的话再用 JS:

.modal {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease-out, transform 0.3s ease-out,
              display 0.3s ease-out allow-discrete;
}

@starting-style {
  .modal {
    opacity: 0;
    transform: translateY(20px);
  }
}

.modal:not([popover]) {
  display: none;
}

用 popover 属性的话,浏览器会自动处理 show/hide,不需要手动管 display,@starting-style 会在元素第一次显示时自动触发。

display: none → visible 的动画

@starting-style 的触发条件是「元素第一次被渲染」,所以对于已经渲染过一次的元素,再次用 display: none 隐藏再显示时,@starting-style 不会再触发——元素会直接跳到正常样式。

如果每次显示都要有入场动画,需要用 popover 属性或者把元素从 DOM 里真正移除再插回去。popover 方案是 2026 年最推荐的做法。

浏览器支持

Chrome 117+、Edge 117+、Firefox 129+、Safari 17.5+ 全部支持,覆盖率已经超过 90%,可以直接在生产环境用。旧版浏览器会跳过动画直接显示元素,属于渐进增强,不影响功能。

下一步

下次要给一个卡片、弹窗、下拉菜单加入场动画时,试试把 JS 监听器删掉,加上 @starting-style 块。超过 90% 的用户会看到流畅的入场动画,剩下的用户看到的是即时出现——比 JS 抖动体验好得多,还省了代码。

评论区

0 条评论

登录后可评论。