必须用 JavaScript 监听 matchMedia 变化并插入临时遮罩层,通过 @keyframes 动画控制 clip-path 从 circle(0 at 50% 50%) 到 circle(150% at 50% 50%) 扩散,动画开始后再切换 data-theme 类以避免闪屏。

深色模式切换时如何触发全屏圆形扩散动画
必须用 prefers-color-scheme 媒体查询监听系统主题变化,但仅靠它无法触发自定义动画——CSS 过渡不响应媒体查询变更。真正可行的路径是:用 JavaScript 监听 matchMedia 变化,手动添加/移除一个 class(比如 dark-mode-enter),再用该 class 触发 CSS 动画。
关键点在于:动画不能绑定在 :root 或 body 上直接改背景色,而要插入一个临时 div 元素作为遮罩层,并控制它的 clip-path 从中心点放大覆盖全屏。
- 监听
window.matchMedia('(prefers-color-scheme: dark)')的addEventListener('change', ...) - 切换前动态创建一个
div,设为position: fixed; inset: 0; z-index: 9999; - 初始状态设
clip-path: circle(0 at 50% 50%);,动画中过渡到circle(150% at 50% 50%) - 动画结束后移除该元素,再切换主题 class,避免视觉卡顿
clip-path 圆形扩散动画为什么必须用 150% 而非 100%
用 circle(100% at 50% 50%) 看似合理,但实际无法完全覆盖视口——因为百分比是相对于元素自身宽高计算的,而 100% 半径只等于元素宽度或高度的一半(取较大者),在宽高比非 1:1 的屏幕上会露出边缘。
更可靠的做法是用足够大的固定值或超大百分比,确保覆盖所有设备:
立即学习“前端免费学习笔记(深入)”;
-
circle(150% at 50% 50%)是最简方案,150% 已远超对角线一半(√2 ≈ 141%),安全冗余 - 避免用
circle(100vh at 50% 50%),因为vh在 iOS Safari 中有滚动缩放问题 - 不要依赖
transform: scale()替代clip-path,否则背景色过渡会不均匀,且无法实现“从中心扩散”的遮罩感
transition 和 animation 在这里哪个更合适
必须用 @keyframes + animation,不能用 transition。因为 clip-path 的起始/结束值不是简单数值(比如 0px → 100px),而是含函数的复杂值(circle(0 at ...) → circle(150% at ...)),多数浏览器不支持对 clip-path 做 transition 插值(Chrome 115+ 开始部分支持,但 Safari 仍不支持)。
实操建议:
- 定义动画:
@keyframes expand-circle { from { clip-path: circle(0 at 50% 50%); } to { clip-path: circle(150% at 50% 50%); } } - 给遮罩层加
animation: expand-circle 0.6s ease-out forwards; - 别忘了加
will-change: clip-path;提升动画性能(尤其低端 Android 设备)
深色模式切换后如何避免闪屏和样式错乱
最常被忽略的是:动画遮罩层出现前,页面已按新主题重绘了一次,导致「先闪一下深色,再盖上动画」。根本解法是延迟主题 class 切换,等动画开始后再执行。
- JavaScript 中,创建遮罩层后立即调用
element.animate(...)或加animationstart事件监听 - 在
animationstart回调里才给document.documentElement添加data-theme="dark"类 - CSS 中所有主题样式必须基于
[data-theme="dark"],而非@media (prefers-color-scheme: dark),否则媒体查询会抢先生效 - 初始 HTML 就带
data-theme="light",避免无样式闪烁(FOUC)


















