你以为 display 只能瞬间消失?今天 CSS 终于会退场动画了

做过 UI 的都有这个痛:弹窗出现有淡入,但关闭时”唰”就没了——因为 display: none 不接受任何过渡,过渡到 display: none 的元素直接消失,动画根本没机会播完。

这件事,CSS 今天终于修了。

离散属性是什么,为什么它不配参与过渡

CSS 里有些属性叫”离散属性”(discrete properties)——它们不在两个值之间插值,而是像开关一样,非此即彼:display: blockdisplay: nonevisibility: visiblevisibility: hiddenoverlay(弹层的控制属性),都是直接跳的,没有中间地带。

历史上,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 组合最直接受益的是:

弹窗 / DialogshowModal()show() 的元素会上 CSS Top Layer,退场时若不处理 overlay 属性,动画会中途截断。

Popoverpopover 属性同样进 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 动画”最近的一次——改动不超过十行代码。


参考来源

评论区

0 条评论

登录后可评论。