目标

  • 在现有 Vite + Vue 3 项目中实现“暗黑模式切换 + 平滑过渡动画”
  • 同时兼顾两种过渡方案:
    • CSS 过渡(简单、通用)
    • View Transitions API(更顺滑的页面级跨淡入淡出,支持的浏览器有加成,自动降级)

适用前提

  • 你已有一个 Vite + Vue3 项目(本教程以 vue3-vt-demo 为例)
  • 关键文件路径:
    • index.html
    • src/main.js
    • src/App.vue
    • src/style.css

实现效果

在这里插入图片描述
完整代码 https://github.com/gzzzxx/vue3-dark


实现思路概览

  1. 使用 data-theme 挂到 html 根节点,驱动全局主题。
  2. 定义一套 CSS 变量(颜色 Token),使用 :root[data-theme="dark"] 切换值。
  3. 提供切换按钮,切换时:
    • 若浏览器支持 document.startViewTransition → 触发视图过渡(更顺滑)
    • 否则使用普通 CSS 过渡(降级方案)
  4. 首屏渲染前,优先从 localStorageprefers-color-scheme 推断主题,避免 FOUC(首屏闪烁)。
  5. 将用户选择持久化到 localStorage

第一步:定义全局颜色 Token 和基础过渡

编辑 src/style.css,加入主题变量与基础过渡(保留你已有内容,添加下列片段即可):

/* 颜色 Token */
:root {
  color-scheme: light;
  --bg: #ffffff;
  --fg: #1f2328;
  --muted: #666;
  --border: #e5e7eb;
  --btn-bg: #f3f4f6;
  --btn-fg: #111827;
  --btn-bg-hover: #e5e7eb;
}

html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0b0f14;
  --fg: #e6edf3;
  --muted: #9aa4af;
  --border: #26323b;
  --btn-bg: #111827;
  --btn-fg: #e6edf3;
  --btn-bg-hover: #1f2937;
}

/* 全局应用 Token */
html, body, #app {
  height: 100%;
}

body {
  background-color: var(--bg);
  color: var(--fg);
  margin: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}

/* 基础过渡(作为 View Transitions 的降级方案) */
body, .card, .btn {
  transition:
    background-color .25s ease,
    color .25s ease,
    border-color .25s ease;
}

/* 示例组件样式(可按需调整) */
.container {
  max-width: 720px;
  margin: 40px auto;
  padding: 24px;
}

.card {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px;
  background: color-mix(in oklab, var(--bg) 96%, transparent);
}

.btn {
  appearance: none;
  border: 1px solid var(--border);
  background: var(--btn-bg);
  color: var(--btn-fg);
  padding: 8px 14px;
  border-radius: 10px;
  cursor: pointer;
}

.btn:hover {
  background: var(--btn-bg-hover);
}

说明:

  • 使用 CSS 变量集中管理颜色,便于主题切换。
  • transition 是 CSS 方案的过渡降级基础。

第二步:在首屏尽早同步初始主题,避免闪烁

index.html 里,给 html 节点准备 data-theme,并在 <head> 里放一个内联脚本(尽早执行):

<!doctype html>
<html lang="zh-CN" data-theme="light">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <!-- 首屏主题同步(localStorage 优先,其次系统偏好) -->
    <script>
      (() => {
        try {
          const stored = localStorage.getItem('theme');
          const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
          const theme = stored ? stored : (prefersDark ? 'dark' : 'light');
          document.documentElement.setAttribute('data-theme', theme);
        } catch (_) {}
      })();
    </script>

    <title>Vue3 暗黑模式 + 过渡动画</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

说明:

  • 该脚本在 Vue 挂载前运行,减少“闪一下”的不一致体验。
  • 主题值来自 localStorage,否则跟随系统。

第三步:在 Vue 中实现切换逻辑(含 View Transitions API)

src/App.vue 中添加切换按钮与逻辑。使用 <script setup> 风格:

<template>
  <div class="container">
    <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
      <h2 style="margin:0;">暗黑模式切换(含过渡)</h2>
      <button class="btn" @click="toggleTheme">
        {{ isDark ? '切换为浅色' : '切换为深色' }}
      </button>
    </div>

    <div class="card" style="margin-top:16px;">
      <p>当前主题:<strong>{{ isDark ? 'dark' : 'light' }}</strong></p>
      <p style="color:var(--muted)">这是一段示例文本,用于观察颜色与过渡。</p>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref } from 'vue'

const theme = ref(
  document.documentElement.getAttribute('data-theme') || 'light'
)

const isDark = computed(() => theme.value === 'dark')

function applyTheme(next) {
  theme.value = next
  document.documentElement.setAttribute('data-theme', next)
  try {
    localStorage.setItem('theme', next)
  } catch (_) {}
}

function toggleTheme() {
  const next = isDark.value ? 'light' : 'dark'

  // 如果支持 View Transitions API,则使用更顺滑的根级过渡
  const startTransition = document.startViewTransition?.bind(document)

  if (startTransition) {
    startTransition(() => {
      applyTheme(next)
    })
    return
  }

  // 否则使用 CSS 过渡(降级)
  applyTheme(next)
}

onMounted(() => {
  // 响应系统主题变更(非必需,可选)
  const mql = window.matchMedia?.('(prefers-color-scheme: dark)')
  if (mql?.addEventListener) {
    mql.addEventListener('change', (e) => {
      const stored = localStorage.getItem('theme')
      // 用户手动选择优先;若未选择,则跟随系统
      if (!stored) applyTheme(e.matches ? 'dark' : 'light')
    })
  }
})
</script>

<style scoped>
/* 局部可按需补充 */
</style>

说明:

  • toggleTheme 会优先使用 document.startViewTransition 包裹主题切换,从而触发页面级过渡。
  • 未支持时回退到 CSS 变量的过渡。

第四步:为 View Transitions API 定制动画(可选增强)

默认的视图过渡是跨淡入淡出。你可以在 src/style.css 中添加对根视图过渡的定制:

/* 自定义视图过渡的时长/缓动 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 250ms;
  animation-timing-function: ease-in-out;
}

/* 如果你想在暗黑切换时做暗场闪烁(可选范例) */
/* html[data-theme="dark"]::view-transition-new(root) {
  animation-name: none; 
}
html[data-theme="light"]::view-transition-new(root) {
  animation-name: none;
} */

说明:

  • 这些伪元素仅在 document.startViewTransition 执行时出现。
  • 若你需要更复杂的主题过渡(比如缩放、方向性擦除),可以给 ::view-transition-* 提供自定义 animation-name 并定义 @keyframes

第五步:入口文件确保全局样式加载

src/main.js 确保引入了全局样式,并正常挂载应用:

import { createApp } from 'vue'
import App from './App.vue'
import './style.css'

createApp(App).mount('#app')

第六步:按钮与主题样式验证

  • 点击按钮时,支持浏览器会看到整页级的平滑过渡(淡入淡出)。
  • 不支持的浏览器也会看到颜色的平滑过渡(基于 CSS transition)。

兼容性与降级策略

  • View Transitions API:目前 Chromium 系列浏览器支持较好;Safari/Firefox 正在推进中。
  • 我们在代码中做了 if (document.startViewTransition) 检测,自动降级到 CSS 过渡。
  • 首屏主题同步脚本保障了“初始主题一致性”,避免闪屏。

常见问题排查

  • 切换不生效:确认 html 节点上的 data-theme 是否随点击变化;CSS 变量是否引用正确。
  • 首屏闪烁:确认 index.html 中的内联脚本是否在 <head> 且早于应用挂载。
  • 样式无过渡:确保 body/.card/.btn 等元素使用了变量色,并存在 transition
  • 自定义动画不生效:查看是否使用了 document.startViewTransition,以及 ::view-transition-old/new(root) 是否正确书写。

完整对照清单

  • index.html:添加首屏内联脚本 + data-theme 属性。
  • src/style.css:添加颜色 Token、基础过渡、可选 View Transitions 定制。
  • src/App.vue:添加切换按钮与逻辑(含 API 检测与降级)。
  • src/main.js:确保全局样式导入与正常挂载。

进阶建议

  • 将主题状态抽离为可复用的 useColorMode() 组合式函数,便于跨组件使用。
  • 利用 color-scheme:has() 等新特性,进一步增强原生控件与主题的适配。
  • 为主题切换按钮增加 aria-pressed 等无障碍属性与键盘交互。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐