进出动画:让浮层平滑出现与消失
模态 <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。关闭时它会从 auto 变 none。若不把它列进 transition 并加
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 锚定坑的处理。