← Dialog / Popover · 浮层与「关闭」的现代解法 / 进出动画:让浮层平滑出现与消失 待审核 4 / 9
animate · @starting-style / allow-discrete

进出动画:让浮层平滑出现与消失

模态 <dialog>popover 默认是瞬间出现 / 消失的。要让它们淡入、滑入、再平滑退出,会撞上两道坎:其一,元素从 display:none 切到可见,而 display离散属性,默认不参与过渡;其二,它们在 top layer 里,关闭时会被立刻移出顶层,退场动画还没播完就被打断。现代 CSS 用三件套解决:@starting-style 给入场首帧、transition-behavior: allow-discrete 让离散属性可过渡、overlay 关键字让元素停留在 top layer 直到动画结束。

本页只讲这套技术在浮层(top layer)上的落地。@starting-style / allow-discrete 本身的机制——为什么 display 这类离散值默认不能补间——见 css-animation · none → block 也能动起来;本页聚焦它叠加 top layer / ::backdrop 后多出来的两个问题。

1 · 三态模型:出场态 / 打开态 / 入场首帧

把样式拆成三段就能描述完整的一来一回。出场态(基样式,即关闭时的样子)、打开态(:open)、入场首帧@starting-style 里的 :open,只在「刚要出现」那一帧生效)。浏览器在「关闭 → 打开」时从入场首帧过渡到打开态,在「打开 → 关闭」时从打开态过渡回出场态——一套样式描述两个方向。

下面是一个真实的模态 <dialog>。用开关分别掐掉入场(@starting-style)与出场(allow-discrete)两段,直接看缺了哪段会怎样。

/* 三态 = 出场态(基) / 打开态(:open) / 入场首帧(@starting-style) */
dialog {
  transition: opacity .34s, translate .34s,
              overlay .34s allow-discrete,   /* 停留在 top layer 直到动画结束 */
              display .34s allow-discrete;   /* 让 display:none 也能过渡 */
}
dialog:not(:open) { opacity: 0; translate: 0 18px; }  /* 出场态 */
dialog:open       { opacity: 1; translate: 0 0; }   /* 打开态 */
@starting-style {
  dialog:open { opacity: 0; translate: 0 18px; }      /* 入场首帧 */
}

/* ::backdrop 同样三态, 跟着一起淡入淡出 */
dialog::backdrop      { background: rgb(43 36 64 / 0);
  transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete; }
dialog:open::backdrop { background: rgb(43 36 64 / .42); }
@starting-style { dialog:open::backdrop { background: rgb(43 36 64 / 0); } }

少了哪段会怎样?关掉 @starting-style:首帧直接等于打开态,入场没有过渡(对话框瞬现),但关闭仍平滑。关掉 allow-discrete:关闭瞬间 display 立即变 none、元素即刻被移出 top layer,出场过渡被切断(对话框瞬消),但入场仍平滑。两段管的是相反的两个方向。

2 · overlay 关键字:别让 top layer 提前撤走

overlay 是个只读属性,表示元素当前是否还在 top layer。关闭时它会从 autonone。若不把它列进 transition 并加 allow-discrete,浏览器会在退场动画播放之前就把元素撤出顶层——元素瞬间掉到普通层叠流里、被其他内容盖住,动画看起来「闪一下就没了」。把 overlayallowdiscreteoverlay \dots allow-discrete 加进过渡,元素就会停留在 top layer 直到动画结束再撤出。单个浮层时它的效果常和 display 重叠,但在浮层相互叠放、或退场途中有其他顶层元素时,缺了它就会暴露问题。

3 · popover 同理:把 :open 换成 :popover-open

Popover 用完全一样的三态写法,只是状态选择器从 :open / :not(:open) 换成 :popover-open / :not(:popover-open)。下面是一个缩放 + 淡入的 popover。

[popover] {
  transition: opacity .25s, scale .25s,
              overlay .25s allow-discrete, display .25s allow-discrete;
}
[popover]:not(:popover-open) { opacity: 0; scale: .92; }   /* 出场态 */
[popover]:popover-open       { opacity: 1; scale: 1; }    /* 打开态 */
@starting-style {
  [popover]:popover-open { opacity: 0; scale: .92; }       /* 入场首帧 */
}

Safari 上的锚定坑。当 popover 用 CSS Anchor Positioning 钉在触发元素旁,部分 Safari 版本在「出场态」会丢失锚点引用,导致退场时位置跳动。解法是在出场态里把 position-anchor / 定位显式写死,而非依赖继承。锚定本身见 anchor-positioning 系列。

4 · 无障碍:prefers-reduced-motion 降级

位移 / 缩放类动画对前庭敏感用户不友好。遵循 prefers-reduced-motion: reduce 时,去掉 translate / scale 这类移动,仅保留不透明度的淡入淡出即可——既尊重偏好,又不至于让浮层硬切。

@media (prefers-reduced-motion: reduce) {
  dialog:not(:open) { translate: 0; }       /* 去掉位移 */
  [popover]:not(:popover-open) { scale: 1; }  /* 去掉缩放, 仅留 opacity */
}

延伸阅读:偏好查询的完整用法见 css-animation · 尊重「减少动态效果」偏好——媒体查询写法、reduce 前后的对比,以及「默认无动效、按需加」的反向组织策略。

延伸阅读:本页两段交互对应 Frontend Masters 的两篇连载:In-N-Out Animations · Dialogs (Part 1)Popovers (Part 2)——把这套三态 + allow-discrete + overlay 的来龙去脉讲得很细,含 ::backdrop 与 Safari 锚定坑的处理。