「Vue 3 + View Transition 实现炫酷圆形缩放换肤动画」

🚀 Vue 3 + View Transition 打造丝滑的换肤动效

在现代 Web 应用里,「深色模式」已经成了标配。但大多数项目的切换主题还是"闪一下",用户体验不够丝滑。

这篇文章我将结合 Vue 3 + VueUse ,再利用 原生 View Transition API ,实现一个带有 圆形扩散/收缩动画 的换肤功能,让主题切换像系统级动画一样自然。目前有很多的类似功能,但从没用过,所以写一下这个功能到项目中。


复制代码
由于是公司项目还没上线,就不贴图到平台中了,但我实现的结果就是这样,如果有好兄弟搞不定的,大家活儿一起研究研究

✨ 背景与思路

传统的主题切换通常有两种方式:

  1. 直接切 CSS class
    比如在 <html> 上加个 dark class,通过 CSS 变量覆盖样式。这种切换非常快,但肉眼可见闪烁。
  2. 加过渡动画(transition)
    在颜色属性上加 transition,能稍微柔和一点,但仍然会出现明显的"前后两帧不一致"的跳变。

而 View Transition API 带来的思路是:

在切换 DOM 状态时,浏览器会自动生成「旧快照」和「新快照」,我们只需要决定 快照如何过渡

这样就能做到类似 截图切换 → 动画过渡 → 新主题渲染 的效果,极大提升用户感受。


🛠 实现步骤

1. 基础主题切换(VueUse)

我们先用 VueUse 提供的 useDarkuseToggle 来管理主题状态:

php 复制代码
const isDark = useDark({
  storageKey: 'useDarkKey', // 本地存储 key
  valueDark: 'dark',
  valueLight: 'light'
});
const toggleDark = useToggle(isDark);

这里的逻辑很简单:isDark 是响应式变量,toggleDark() 用来切换主题。

对应的 CSS 里,我们只要写好 :roothtml.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 变量与主题适配

只需在 :roothtml.dark 中定义一组变量:

css 复制代码
:root {
  --dashboard-bg-color: #f5f7f8;
  --dashboard-title-color: #000;
  // ...
}

html.dark {
  --dashboard-bg-color: #002d6d;
  --dashboard-title-color: #fff;
  // ...
}

这样,组件里所有颜色都通过 var(--xxx) 读取,就能随着主题一键切换。

📌


🔧 优化点

  1. 兼容性处理
    View Transition API 目前只在 Chromium 系列浏览器可用。代码里已经做了 fallback(直接切换)。
  2. 动画起点
    动画的圆心取点击坐标 (x, y),这样有"从点击处扩散"的效果。如果用户不是点击,而是快捷键触发,就默认屏幕中心。
  3. 过渡顺序
    通过 data-vt-from + CSS,保证新旧快照的层级控制,避免出现叠放问题。

🎯 最终效果

  • 点击「切换主题」 → 从点击点扩散动画 → 平滑进入暗黑/亮色模式。
  • 不支持 API 的浏览器 → 直接切换,保证功能可用。
  • 颜色管理统一使用 CSS 变量,方便全局维护。

📌


📝 总结

相比于传统的闪切,View Transition API 能让换肤的体验 "无缝 + 丝滑" ,而且配合 VueUse 的封装,代码量也不大。

如果你正在做 后台管理系统 / 数据大屏 / 桌面风格应用,强烈推荐尝试这种换肤方式。

相关推荐
猫七先生1 天前
在 React 里用 PMTiles 自托管底图
前端
tanghonghanhaoli1 天前
【图书翻译】有意义的游戏设计:桌面游戏的方法论与心理学(第1、2章)
前端·游戏·ui
光影少年1 天前
react native打包优化、包体积瘦身、离线包/热更新方案
前端·react native·react.js
Cache技术分享1 天前
504. Java 反射 - 创建一个简单的依赖注入框架
前端·后端
_codeOH1 天前
# 手把手复刻 DeepSeek 官网效果:玻璃拟态 + WebGL 流体 + Spring 动画,一篇讲透
前端
名字还没想好☜1 天前
Next.js 用 cookies()/headers() 读请求信息:动态渲染触发、缓存失效与在 Server Action 里读写 cookie
前端·javascript·缓存·react·next.js·app router
穿过生命散发芬芳1 天前
用 TRAE Work 把零散运维知识,系统化成一本可复用的运维红宝书
人工智能·trae
今日无bug1 天前
从「拿来主义」到「亲手造轮子」:2 种 MCP 文件服务器写法对比
前端·node.js·mcp
xcyxiner1 天前
flutter 运行到模拟器上
android·前端·flutter
用户921080262861 天前
从 COT 到 ThoughtChain:AI 应用为什么需要展示“思考过程”
前端