使用 VueUse 和 View Transitions API 实现暗黑模式主题动画切换效果

前言

在当今的 Web 应用中,暗黑模式已经成为一种常见且受欢迎的功能。它不仅能够减少用户在夜间使用应用时的眼睛疲劳,还能为应用增添一份现代感。然而,简单的主题切换可能会显得生硬,影响用户体验。本文将介绍如何使用 VueUse 库和 View Transitions API 实现一个平滑的暗黑模式切换动画效果,让主题切换变得更加丝滑和优雅。

效果展示

技术栈介绍

View Transitions API

View Transitions API 是浏览器原生提供的一种 API,它能够让我们在 DOM 元素发生变化时创建平滑的过渡动画。这个 API 特别适合用于页面切换、主题切换等场景,能够大大提升用户体验。

兼容性:目前 View Transitions API 已经在 ChromeEdge 等主流浏览器中得到支持,但在 SafariFirefox 中可能尚未完全支持。Safari 仅在去年9月发布的 18 版本及以上支持。

VueUse

VueUse 是一个基于 Vue 的组合式 API 工具库,提供了大量实用的 Vue 组合式函数,能够帮助我们简化开发过程。在本文中,我们将使用 VueUse 提供的 useDarkuseToggle 函数来实现主题切换功能。

NOTE

点击下发按钮,或在页面任意位置右键切换主题,即可查看切换效果,

loading

实现步骤

1. 安装依赖

首先,我们需要安装 VueUse 库:

bash
# 使用 npm
npm install @vueuse/core

# 或使用 pnpm
pnpm install @vueuse/core

# 或使用 yarn
yarn add @vueuse/core

2. 创建主题切换组件

接下来,我们创建一个主题切换组件,例如 ThemeToggle.vue

vue
<template>
  <div class="theme-container" ref="container">
    <button class="toggle-theme" @click="toggleTheme">
      <div class="icon">
        {{ isDark ? '🌞' : '🌙' }}
      </div>
    </button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDark, useToggle } from '@vueuse/core'

// 使用 VueUse 的 useDark 函数来管理暗黑模式状态
const isDark = useDark({
  selector: 'html',
  attribute: 'data-bs-theme',
  valueDark: 'dark',
  valueLight: 'light'
})

// 使用 useToggle 函数来切换暗黑模式
const toggleDark = useToggle(isDark)

// 获取容器引用
const container = ref(null)

// 主题切换函数
const toggleTheme = (event) => {
  // 获取点击位置坐标
  const x = event.clientX
  const y = event.clientY
  
  // 计算结束半径(从点击位置到屏幕最远点的距离)
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  )

  // 兼容性处理:如果浏览器不支持 View Transitions API,则直接切换主题
  if (!document.startViewTransition) {
    toggleDark()
    return
  }

  // 使用 View Transitions API 创建过渡效果
  const transition = document.startViewTransition(async () => {
    toggleDark()
  })

  // 过渡准备就绪后,执行动画
  transition.ready.then(() => {
    // 定义圆形裁剪路径
    const clipPath = [
      `circle(0px at ${x}px ${y}px)`,
      `circle(${endRadius}px at ${x}px ${y}px)`,
    ]
    
    // 根据当前主题状态决定动画方向
    document.documentElement.animate(
      {
        clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
      },
      {
        duration: 400,
        easing: "ease-in",
        pseudoElement: isDark.value
          ? "::view-transition-old(root)"
          : "::view-transition-new(root)",
      }
    )
  })
}
</script>

<style>
.theme-container {
  position: relative;
  overflow: hidden;
}

.toggle-theme {
  cursor: pointer;
  border: none;
  background: transparent;
  padding: 8px;
  border-radius: 50%;
  transition: background-color 0.3s;
}

.toggle-theme:hover {
  background-color: rgba(0, 0, 0, 0.1);
}

.icon {
  font-size: 20px;
}
</style>

3. 添加 CSS 样式

为了确保 View Transitions API 正常工作,我们需要添加一些 CSS 样式。这些样式可以放在全局样式文件中,例如 main.cssApp.vue<style> 标签中:

css
/* 禁用默认的 View Transitions 动画 */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 设置亮色模式下的层级顺序 */
::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 2147483646;
}

/* 设置暗色模式下的层级顺序(与亮色模式相反) */
[data-bs-theme="dark"]::view-transition-old(root) {
  z-index: 2147483646;
}

[data-bs-theme="dark"]::view-transition-new(root) {
  z-index: 1;
}

4. 在主应用中使用主题切换组件

最后,我们需要在主应用中使用这个主题切换组件:

vue
<template>
  <div class="app">
    <header>
      <h1>我的应用</h1>
      <ThemeToggle />
    </header>
    <main>
      <!-- 应用内容 -->
    </main>
  </div>
</template>

<script setup>
import ThemeToggle from './components/ThemeToggle.vue'
</script>

<style>
/* 定义主题变量 */
:root {
  --bg-color: #ffffff;
  --text-color: #1a1a1a;
  --card-bg: #f5f5f5;
  --border-color: #e0e0e0;
}

:root[data-bs-theme="dark"] {
  --bg-color: #1a1a1a;
  --text-color: #ffffff;
  --card-bg: #2d2d2d;
  --border-color: #404040;
}

/* 应用主题变量 */
body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s, color 0.3s;
}

.app {
  min-height: 100vh;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
}

main {
  padding: 2rem;
}

/* 卡片样式 */
.card {
  background-color: var(--card-bg);
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: background-color 0.3s, box-shadow 0.3s;
}
</style>

原理解析

View Transitions API 工作原理

  1. 捕获快照:当调用 document.startViewTransition() 时,浏览器会捕获当前 DOM 状态的快照。

  2. 执行 DOM 更新:在回调函数中,我们执行 DOM 更新(例如切换主题)。

  3. 捕获新状态:浏览器捕获更新后的 DOM 状态快照。

  4. 创建过渡:浏览器使用这两个快照创建过渡动画。

  5. 自定义动画:我们可以通过 CSS 伪元素 ::view-transition-old::view-transition-new 来自定义过渡动画。

圆形扩散效果实现

在我们的实现中,我们使用了 clipPath 属性来创建圆形扩散效果:

  1. 计算点击位置:获取用户点击的坐标。

  2. 计算结束半径:计算从点击位置到屏幕最远点的距离,确保圆形能够覆盖整个屏幕。

  3. 创建圆形裁剪路径:定义从点击位置开始的圆形裁剪路径,从 0 半径扩展到结束半径。

  4. 根据主题状态调整动画方向:根据当前主题状态决定动画的方向(从亮色到暗色或从暗色到亮色)。

兼容性处理

由于 View Transitions API 尚未在所有浏览器中得到支持,我们需要添加兼容性处理:

javascript
// 兼容性处理
if (!document.startViewTransition) {
  toggleDark();
  return;
}

这样,在不支持 View Transitions API 的浏览器中,主题切换仍然可以正常工作,只是没有动画效果。

优化建议

  1. 调整动画时长和缓动函数:可以根据需要调整动画的时长和缓动函数,以获得最佳的视觉效果。

  2. 添加更多主题变量:可以添加更多的 CSS 变量来控制不同元素的颜色,使主题切换更加灵活。

  3. 保存用户偏好:可以使用 localStoragesessionStorage 来保存用户的主题偏好,以便在下次访问时自动应用。

  4. 添加系统主题跟随:可以使用 prefers-color-scheme 媒体查询来检测系统主题,并自动应用相应的主题。

希望本文能够帮助你实现一个漂亮的暗黑模式主题切换效果!如果有任何问题或建议,欢迎在评论区留言。