🚀 Vue 3 + View Transition 打造丝滑的换肤动效
在现代 Web 应用里,「深色模式」已经成了标配。但大多数项目的切换主题还是"闪一下",用户体验不够丝滑。
这篇文章我将结合 Vue 3 + VueUse ,再利用 原生 View Transition API ,实现一个带有 圆形扩散/收缩动画 的换肤功能,让主题切换像系统级动画一样自然。目前有很多的类似功能,但从没用过,所以写一下这个功能到项目中。
由于是公司项目还没上线,就不贴图到平台中了,但我实现的结果就是这样,如果有好兄弟搞不定的,大家活儿一起研究研究
✨ 背景与思路
传统的主题切换通常有两种方式:
- 直接切 CSS class
比如在<html>
上加个dark
class,通过 CSS 变量覆盖样式。这种切换非常快,但肉眼可见闪烁。 - 加过渡动画(transition)
在颜色属性上加 transition,能稍微柔和一点,但仍然会出现明显的"前后两帧不一致"的跳变。
而 View Transition API 带来的思路是:
在切换 DOM 状态时,浏览器会自动生成「旧快照」和「新快照」,我们只需要决定 快照如何过渡。
这样就能做到类似 截图切换 → 动画过渡 → 新主题渲染 的效果,极大提升用户感受。
🛠 实现步骤
1. 基础主题切换(VueUse)
我们先用 VueUse 提供的 useDark
和 useToggle
来管理主题状态:
php
const isDark = useDark({
storageKey: 'useDarkKey', // 本地存储 key
valueDark: 'dark',
valueLight: 'light'
});
const toggleDark = useToggle(isDark);
这里的逻辑很简单:isDark
是响应式变量,toggleDark()
用来切换主题。
对应的 CSS 里,我们只要写好 :root
和 html.dark
的变量覆盖即可。
2. 引入 View Transition API
在点击主题切换按钮时,我们触发一个动画。核心代码:
ts
async function toggleTheme(e: MouseEvent) {
const htmlEl = document.documentElement;
const x = e?.clientX ?? window.innerWidth / 2;
const y = e?.clientY ?? window.innerHeight / 2;
const fromDark = isDark.value;
htmlEl.setAttribute('data-vt-from', fromDark ? 'dark' : 'light');
if (!('startViewTransition' in document)) {
toggleDark();
htmlEl.removeAttribute('data-vt-from');
return;
}
const transition = (document as any).startViewTransition(() => {
toggleDark();
});
await transition.ready;
const radius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
);
const expand = [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`];
const collapse = [`circle(${radius}px at ${x}px ${y}px)`, `circle(0px at ${x}px ${y}px)`];
let anim: Animation;
if (fromDark) {
anim = htmlEl.animate({ clipPath: collapse }, {
duration: 600,
easing: 'ease-in-out',
pseudoElement: '::view-transition-old(root)',
fill: 'both'
} as any);
} else {
anim = htmlEl.animate({ clipPath: expand }, {
duration: 600,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
fill: 'both'
} as any);
}
await anim.finished;
htmlEl.removeAttribute('data-vt-from');
}
这里有几个关键点:
startViewTransition
:浏览器快照切换的入口。pseudoElement
:通过::view-transition-old(root)
/::view-transition-new(root)
区分新旧快照。- clipPath 动画:用圆形扩散/收缩的方式做出「聚焦」的感觉。

3. CSS 变量与主题适配
只需在 :root
和 html.dark
中定义一组变量:
css
:root {
--dashboard-bg-color: #f5f7f8;
--dashboard-title-color: #000;
// ...
}
html.dark {
--dashboard-bg-color: #002d6d;
--dashboard-title-color: #fff;
// ...
}
这样,组件里所有颜色都通过 var(--xxx)
读取,就能随着主题一键切换。
📌

🔧 优化点
- 兼容性处理
View Transition API 目前只在 Chromium 系列浏览器可用。代码里已经做了 fallback(直接切换)。 - 动画起点
动画的圆心取点击坐标(x, y)
,这样有"从点击处扩散"的效果。如果用户不是点击,而是快捷键触发,就默认屏幕中心。 - 过渡顺序
通过data-vt-from
+ CSS,保证新旧快照的层级控制,避免出现叠放问题。
🎯 最终效果
- 点击「切换主题」 → 从点击点扩散动画 → 平滑进入暗黑/亮色模式。
- 不支持 API 的浏览器 → 直接切换,保证功能可用。
- 颜色管理统一使用 CSS 变量,方便全局维护。
📌

📝 总结
相比于传统的闪切,View Transition API 能让换肤的体验 "无缝 + 丝滑" ,而且配合 VueUse 的封装,代码量也不大。
如果你正在做 后台管理系统 / 数据大屏 / 桌面风格应用,强烈推荐尝试这种换肤方式。