CSS @starting-style 怎样为弹窗补上首次进入动画
来源:17golang原创
时间:2026-11-01 10:47:57 440浏览 收藏
弹窗第一次出现时,很多人会发现 transition 写了却没有淡入:元素之前没有可供比较的已渲染状态,或者它从 display: none 直接变成可见状态,浏览器没有普通过渡所需的“上一帧”。解决办法是用 @starting-style 明确提供首次进入的起始值,再把 display 配进允许离散变化的过渡中。
官方资料:https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40starting-style
把弹窗动画拆成默认隐藏态、首次进入起始态和打开完成态:@starting-style只负责没有上一帧时从哪里开始,transition-behavior: allow-discrete负责让display在动画边界翻转。这样进入时能淡入,退出时也不会立即消失。
先区分首次进入与普通返回
普通过渡通常是“旧样式 → 新样式”。但 dialog.showModal() 会让弹窗从不可见状态进入顶层,首次显示时并没有一个已经绘制过的可见样式可以当作起点。此时只写下面的最终样式和 transition,弹窗仍可能直接出现:
.settings-dialog {
/* 让透明度和位移变化使用同一段过渡 */
transition: opacity 220ms ease, transform 220ms ease;
opacity: 1;
transform: translateY(0);
}
@starting-style 的职责是为“第一次样式更新”提供起始值。它不是永久状态,也不是替代默认样式的隐藏规则;弹窗已经显示过以后,关闭再打开时会回到默认隐藏态和新的进入起始态之间的过渡。

给 dialog 建立三种 CSS 状态
先只处理透明度和轻微位移,不急着加入复杂阴影或缩放。默认的 dialog 状态保持隐藏并准备退出;dialog:open 描述打开后的最终状态;嵌套在打开规则里的 @starting-style 则给第一次进入提供透明和位移起点:
.settings-dialog {
/* 默认状态用于关闭后的回退,也作为退出动画的终点 */
opacity: 0;
transform: translateY(12px) scale(0.98);
transition:
opacity 220ms ease,
transform 220ms ease,
display 220ms allow-discrete;
}
.settings-dialog:open {
/* 打开状态是进入动画的终点 */
opacity: 1;
transform: translateY(0) scale(1);
@starting-style {
/* 首次显示没有上一帧时,从这里开始过渡 */
opacity: 0;
transform: translateY(12px) scale(0.98);
}
}
这里有一个容易混淆的地方:进入时使用的是 @starting-style → :open,关闭时则是 :open → 默认规则。也就是说,@starting-style 不会在每一次普通样式变化中持续介入,不能把它当成永久的“关闭状态”。
把 display 放进离散过渡
如果弹窗在退出时立刻变成 display: none,透明度还没有来得及变成 0,元素就已经从渲染树中消失,用户看到的结果就是“没有淡出”。将 display 列入 transition,并使用 allow-discrete,可以让这个离散属性在动画边界翻转,而不是用连续插值。
.settings-dialog {
/* display 不是连续数值,所以显式允许离散过渡 */
transition-behavior: allow-discrete;
transition:
opacity 220ms ease,
transform 220ms ease,
display 220ms allow-discrete;
}
.settings-dialog:not(:open) {
/* 关闭状态保留到退出动画结束,再由 display 隐藏 */
display: none;
}
在从隐藏到可见的方向,display 会在动画开始时切到可见值,使透明度和位移有机会播放;从可见到隐藏的方向,display 会尽量延迟到动画结束再翻转。这样做的关键不是给 display 设置一个“中间值”,而是让它配合连续属性的可见时间窗口。

用最小 HTML、CSS 和 JavaScript 连接状态
下面的示例只负责打开和关闭原生弹窗。JavaScript 不参与动画计算,只改变 dialog 的生命周期状态;动画全部交给 CSS。代码中的注释说明了打开、关闭和资源监听的职责边界:
const openButton = document.querySelector("#open-settings");
const dialog = document.querySelector("#settings-dialog");
// 打开动作只改变 dialog 状态,进入动画由 CSS 接管。
openButton.addEventListener("click", () => {
if (!dialog.open) {
dialog.showModal();
}
});
// 原生 form method="dialog" 会调用 close(),无需手工移除节点。
dialog.addEventListener("close", () => {
// 这里可以同步表单结果,但不要在此处直接设置 display:none。
console.log("dialog result:", dialog.returnValue);
});
如果你使用自定义按钮而不是 method="dialog",关闭时调用 dialog.close("cancel") 即可。不要为了播放动画先把节点删除;节点一旦被删除,就没有退出过渡可以承载了。
兼容与降级边界
@starting-style 和离散属性过渡属于较新的 CSS 能力。写稿时 MDN 将 @starting-style 标为 Baseline 2024,但旧浏览器或受限运行环境仍可能不支持。降级策略应优先保证弹窗能打开和关闭,再把动画当作增强:
/* 不支持新规则时,至少保留可见的原生 dialog 行为 */
.settings-dialog {
opacity: 1;
transform: none;
}
@supports (transition-behavior: allow-discrete) {
/* 支持离散过渡的环境再启用 display 与进入起始值 */
.settings-dialog {
opacity: 0;
transform: translateY(12px);
transition:
opacity 220ms ease,
transform 220ms ease,
display 220ms allow-discrete;
}
.settings-dialog:open {
opacity: 1;
transform: translateY(0);
@starting-style {
/* 只在支持这组特性时提供首次进入起点 */
opacity: 0;
transform: translateY(12px);
}
}
}
还要注意用户的减少动态效果设置。弹窗是信息入口,不能因为动画而降低可读性:
@media (prefers-reduced-motion: reduce) {
/* 减少动态时缩短过渡,保留状态变化和可访问性 */
.settings-dialog {
transition-duration: 1ms;
}
}
排查清单:动画仍然不出现时看哪里
- 确认选择器确实命中了同一个
dialog,不要把@starting-style写到另一个组件作用域里。 - 确认进入和退出的连续属性有明确的默认值与打开值,至少先检查
opacity和transform。 - 确认
display出现在过渡列表中,并且使用了allow-discrete。 - 确认关闭逻辑没有同步删除节点、设置内联
display:none或跳过原生close()生命周期。 - 确认页面没有启用
prefers-reduced-motion分支后把时长压到几乎不可见。
最小可用的判断标准是:首次打开能从透明、偏移状态进入;重复关闭时仍能看见短暂退出;不支持新特性时弹窗仍然可以正常使用。达到这三个条件,再考虑阴影、遮罩和更复杂的层级动画。
常见问题
@starting-style 能替代 CSS animation 吗?
它主要服务于 CSS transition 的首次样式更新。若使用关键帧动画,起始帧可以直接写在 from 或 0% 中,不需要用 @starting-style。
为什么打开动画有了,关闭动画还是没有?
通常是关闭时立即变成 display:none 或节点被删除。把 display 作为允许离散过渡的属性,并让关闭状态留到过渡完成,才能给透明度和位移留下播放时间。
必须使用原生 dialog 才能用这个规则吗?
不是。任何首次加入 DOM 或从不可见状态变为可见的元素都可以使用,但原生 dialog 的打开、关闭和顶层显示场景更容易暴露“没有上一帧”的问题。
不支持 allow-discrete 时应该怎么办?
保留无动画的可见状态作为降级,或者用 JavaScript 在状态切换后再添加类名。不要让兼容分支阻止用户打开和关闭弹窗。
-
427 收藏
-
219 收藏
-
116 收藏
-
405 收藏
-
262 收藏
-
443 收藏
-
167 收藏
-
469 收藏
-
159 收藏
-
443 收藏
-
357 收藏
-
181 收藏
-
134 收藏
-
351 收藏
-
281 收藏
-
281 收藏
-
397 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习