你以为 display 只能瞬间消失?今天 CSS 终于会退场动画了
做过 UI 的都有这个痛:弹窗出现有淡入,但关闭时”唰”就没了——因为 display: none 不接受任何过渡,过渡到 display: none 的元素直接消失,动画根本没机会播完。
这件事,CSS 今天终于修了。
离散属性是什么,为什么它不配参与过渡
CSS 里有些属性叫”离散属性”(discrete properties)——它们不在两个值之间插值,而是像开关一样,非此即彼:display: block 和 display: none、visibility: visible 和 visibility: hidden、overlay(弹层的控制属性),都是直接跳的,没有中间地带。
历史上,transition 遇到这些属性会直接放弃:它会正常过渡 opacity、transform、color 这些可插值属性,但一碰到 display 或 visibility,直接”跳帧”——不报错,不警告,直接不等动画播完就把元素从 DOM 里拔掉。
所以长期以来,想让弹窗优雅退场,唯一的办法是:
- 不用
display: none,改用visibility: hidden+opacity: 0骗过眼睛,但 DOM 节点还占着位 - 或者直接上 JavaScript,等动画
transitionend再手动设display: none
两行 CSS,把退场动画原生化了
Chrome 117 / Firefox 129 / Safari 17.4 带来了 transition-behavior: allow-discrete,字面意思:允许离散属性也参与过渡。
/* 基础写法:在 transition 简写里加 allow-discrete */
.modal {
transition: opacity 0.3s ease, transform 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
.modal.is-closed {
opacity: 0;
transform: scale(0.95);
display: none; /* 这里不会"唰"消失了 */
/* overlay 等动画播完再移除 */
}
allow-discrete 本质上是把 display: none 的切换时机从”立刻”推迟到了”过渡中点”(50% 处),这样 opacity 和 transform 的淡出动画才能完整播完,元素再消失。
配合 @starting-style,把入场动画也补上
只有退场还不够。入场动画同样有问题:元素从 display: none 变回来时,浏览器根本没有”上一帧”可以过渡,所以入场动画也不会触发。
@starting-style 就是来解决这个的——告诉浏览器:元素第一次出现时,应该从哪个状态开始过渡。
/* 弹窗关闭状态 */
.modal {
opacity: 0;
transform: translateY(8px) scale(0.95);
transition: opacity 0.25s ease, transform 0.25s ease,
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
/* 弹窗打开状态 */
.modal[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 入场起点:定义"从哪来" */
@starting-style {
.modal[open] {
opacity: 0;
transform: translateY(8px) scale(0.95);
}
}
这一套组合拳下来,display/visibility/overlay 三个离散属性终于能跟 opacity 和 transform 一样参与完整的入场+退场动画了。
适用场景:不只是弹窗
allow-discrete + @starting-style 组合最直接受益的是:
弹窗 / Dialog:showModal() 和 show() 的元素会上 CSS Top Layer,退场时若不处理 overlay 属性,动画会中途截断。
Popover:popover 属性同样进 Top Layer,加上 overlay allow-discrete 才能保证关闭时弹层消失和动画同步。
抽屉 Drawer / 侧边栏:从屏幕外滑入、滑出,display 切换全程动画化,不需要 JS 手动管理。
手风琴 Accordion:高度从 0 到 auto 的动画,传统方案只能用 max-height hack,现在直接配合 display allow-discrete。
懒加载内容区:content-visibility: auto 配合离散过渡,可以优雅地把大列表的渲染时机交给滚动行为。
三个坑
1. 写在 transition 简写后面的 transition-behavior 会被覆盖
/* ✗ 错误:后面的 transition 简写把前面的 transition-behavior 覆盖了 */
.card {
transition-behavior: allow-discrete; /* 会被重置 */
transition: opacity 0.3s;
}
/* ✓ 正确:transition-behavior 放在 transition 简写之后 */
.card {
transition: opacity 0.3s, display 0.3s allow-discrete;
transition-behavior: allow-discrete;
}
2. overlay 属性不只是”装饰”
overlay 控制元素在 Top Layer 中的层级。没有 overlay allow-discrete,关闭 dialog 时弹层立刻消失,用户会看到背景色闪一下才消失。保留 overlay 过渡,关闭动画才是完整的。
3. 渐进增强还是要做
Firefox 和 Safari 老版本不支持。可以用 @supports 做能力检测:
.modal {
/* 兜底:直接消失 */
transition: opacity 0.25s;
}
@supports (transition-behavior: allow-discrete) {
.modal {
transition: opacity 0.25s ease, transform 0.25s ease,
display 0.25s allow-discrete, overlay 0.25s allow-discrete;
}
}
浏览器支持
Chrome 117+、Firefox 129+、Safari 17.4+ 已支持,全球覆盖率约 91%(2026 年 9 月数据)。@starting-style 同样三位一体支持。
下一步
去你自己的项目里找三个用了 display: none 做显示/隐藏的组件:
- 弹窗 / Modal
- Popover / Tooltip
- 折叠面板 / Accordion
把它们改写成 transition-behavior: allow-discrete + @starting-style,从”唰一下消失”变成”优雅退场”。这大概是你距离”原生 CSS 动画”最近的一次——改动不超过十行代码。
参考来源:
- Four new CSS features for smooth entry/exit animations(Chrome Developers,官方文档)
- transition-behavior – MDN Web Docs(MDN,完整示例)
- Baseline: Entry Animations Now Widely Available(web.dev,浏览器支持数据)
- Using the Popover API – MDN(MDN,Popover + overlay 实战)
评论区
登录后可评论。