Vite + Vue 3 项目中实现“暗黑模式切换 + 平滑过渡动画”
·
目标
- 在现有 Vite + Vue 3 项目中实现“暗黑模式切换 + 平滑过渡动画”
- 同时兼顾两种过渡方案:
- CSS 过渡(简单、通用)
- View Transitions API(更顺滑的页面级跨淡入淡出,支持的浏览器有加成,自动降级)
适用前提
- 你已有一个 Vite + Vue3 项目(本教程以
vue3-vt-demo为例) - 关键文件路径:
index.htmlsrc/main.jssrc/App.vuesrc/style.css
实现效果

完整代码 https://github.com/gzzzxx/vue3-dark
实现思路概览
- 使用
data-theme挂到html根节点,驱动全局主题。 - 定义一套 CSS 变量(颜色 Token),使用
:root与[data-theme="dark"]切换值。 - 提供切换按钮,切换时:
- 若浏览器支持
document.startViewTransition→ 触发视图过渡(更顺滑) - 否则使用普通 CSS 过渡(降级方案)
- 若浏览器支持
- 首屏渲染前,优先从
localStorage或prefers-color-scheme推断主题,避免 FOUC(首屏闪烁)。 - 将用户选择持久化到
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等无障碍属性与键盘交互。
更多推荐


所有评论(0)