如何根据系统主题自动响应CSS深色模式
# 一、原理(一句话)
浏览器提供了媒体查询 prefers-color-scheme,用来感知系统(或用户代理)偏好“浅色”或“深色”主题。配合 CSS 自定义属性和少量 JS,可以做到自动响应并支持用户手动切换与持久化。
# 二、CSS-only(最简单)
浏览器会根据系统偏好匹配 @media (prefers-color-scheme: dark)。
:root {
--bg: #ffffff;
--text: #111827;
--muted: #6b7280;
}
/* 深色主题覆盖 */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b1220;
--text: #e6eef6;
--muted: #9aa6b2;
}
}
/* 使用变量 */
body {
background: var(--bg);
color: var(--text);
}
a { color: var(--text); }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
优点:实现简单、自动响应系统设置;缺点:不能持久化用户手动选择(也不能在页面上给用户显式开关)。
# 三、推荐做法:CSS 变量 + data-theme(支持自动、手动与持久化)
# CSS(变量与 class/data-attribute)
:root {
--bg: #ffffff;
--text: #111827;
--muted: #6b7280;
color-scheme: light; /* 告诉 UA 表单控件等使用哪种配色 */
}
/* 深色主题定义(用于手动或 JS 切换) */
[data-theme="dark"] {
--bg: #0b1220;
--text: #e6eef6;
--muted: #9aa6b2;
color-scheme: dark;
}
/* 保持自动响应:如果用户系统偏好是 dark,但没有手动设置,则也应用深色变量 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg: #0b1220;
--text: #e6eef6;
--muted: #9aa6b2;
color-scheme: dark;
}
}
body {
background: var(--bg);
color: var(--text);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
说明:
data-theme="dark"用于用户显式切换时覆盖系统偏好。:root:not([data-theme])这句确保只有在用户未手动设置时,才根据系统偏好自动生效(优先级清晰)。
# JS:初始化、切换、持久化、监听系统变更
const THEME_KEY = 'site-theme';
function applyTheme(theme) {
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
} else {
document.documentElement.removeAttribute('data-theme');
}
}
// 读取持久化的用户首选项(localStorage)
function getStoredTheme() {
return localStorage.getItem(THEME_KEY); // 'dark' | 'light' | null
}
// 写入
function storeTheme(theme) {
if (theme) localStorage.setItem(THEME_KEY, theme);
else localStorage.removeItem(THEME_KEY);
}
// 初始逻辑:优先用用户设置,其次用系统偏好
function initTheme() {
const stored = getStoredTheme();
if (stored) {
applyTheme(stored);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
// 如果不想写 data-theme,使用 :root:not([data-theme]) 的 CSS 也能处理
// 这里可以不做任何事(由 CSS 的 media query 控制),或者显式设置 null
applyTheme(null);
}
}
initTheme();
// 切换接口(可绑定到按钮)
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
storeTheme(next);
}
// 监听系统主题变化(用于未手动设置时自动切换)
if (window.matchMedia) {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', e => {
const stored = getStoredTheme();
if (stored) return; // 用户已手动选择,忽略系统变化
if (e.matches) {
applyTheme(null); // 让 CSS media query 生效(或显式 applyTheme('dark'))
} else {
applyTheme(null);
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 四、避免闪烁(FOUC / 抖动)——服务端渲染 / 初始渲染注意
当页面初次加载时,如果 CSS 或 JS 慢,可能会先渲染错误主题再切换。常用做法是在 <head> 放一小段同步脚本,尽早把 data-theme 写到 document.documentElement,在 CSS 加载前就决定主题:
<script>
(function(){
try {
const theme = localStorage.getItem('site-theme');
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
// 不写 data-theme 则由 CSS media query 控制;如果想显式,也可以 setAttribute('data-theme','dark')
// document.documentElement.setAttribute('data-theme','dark');
}
} catch(e){}
})();
</script>
2
3
4
5
6
7
8
9
10
11
12
13
把这段内联放在 CSS <link> 之前可以避免“先白底后暗”的闪烁。
# 五、扩展与兼容性注意点
color-scheme:在 root 或 [data-theme] 上设置color-scheme: dark有助于浏览器渲染表单控件、滚动条、placeholder 等系统组件颜色一致。图片 / svg / icon:
- 使用支持双主题的图片:
<picture>或image-set(),或在 CSS 中根据[data-theme="dark"]切换background-image。 - SVG 用
currentColor填充,方便随文字颜色切换。
- 使用支持双主题的图片:
第三方组件:检查组件库是否支持主题切换(Element Plus、Ant Design Vue 等通常有主题机制)。
无
prefers-color-scheme的浏览器:仍然可通过 CSS 变量 + JS 切换兼容。Accessibility:确保深色模式下对比度足够(WCAG 指导),图片和图标在深色下仍可见。
过渡/动画:切主题时不建议对背景色做长时间过渡,避免闪烁或重绘负担。可以短时间淡入淡出,但不要影响首屏渲染。
# 六、面试中可说的精炼回答(30s)
“我们通过
@media (prefers-color-scheme: dark)让页面默认跟随系统主题,同时使用 CSS 变量和[data-theme]支持用户手动切换并持久化到 localStorage。页面初始化时用内联脚本优先设置data-theme防止主题闪烁,并监听matchMedia('(prefers-color-scheme: dark)')的change事件在用户未手动设置时同步系统变化。补充使用color-scheme、SVG 的currentColor与双图像策略保证视觉一致性与可访问性。”
# 七、在vue3+Composition API中应用
下面我给你一个可直接复制使用的 Vue 3 + Composition API 实现,包括:
- ✅ 自动检测系统主题
- ✅ 用户手动切换 + 本地持久化
- ✅ 响应式实时更新
- ✅ 避免闪烁(SSR/首屏优化)
# 一、目录结构建议
src/
├─ composables/
│ └─ useTheme.ts ← 主题切换逻辑
├─ components/
│ └─ ThemeToggle.vue ← 切换按钮
├─ main.ts ← 初始化主题(防闪烁)
└─ App.vue
2
3
4
5
6
7
# 二、useTheme.ts(核心逻辑)
// src/composables/useTheme.ts
import { ref, watchEffect, onMounted, onBeforeUnmount } from 'vue'
const THEME_KEY = 'site-theme' // localStorage 键名
type ThemeType = 'light' | 'dark' | null
export function useTheme() {
const theme = ref<ThemeType>(null) // 当前主题
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
/** 应用到 HTML 根节点 */
const applyTheme = (t: ThemeType) => {
const el = document.documentElement
if (t === 'dark') {
el.setAttribute('data-theme', 'dark')
el.style.colorScheme = 'dark'
} else if (t === 'light') {
el.setAttribute('data-theme', 'light')
el.style.colorScheme = 'light'
} else {
el.removeAttribute('data-theme')
el.style.colorScheme = prefersDark.matches ? 'dark' : 'light'
}
}
/** 从本地读取用户设置 */
const getStoredTheme = (): ThemeType => {
const value = localStorage.getItem(THEME_KEY)
return value === 'dark' || value === 'light' ? value : null
}
/** 切换主题 */
const toggleTheme = () => {
const next = theme.value === 'dark' ? 'light' : 'dark'
theme.value = next
localStorage.setItem(THEME_KEY, next)
}
/** 恢复默认(跟随系统) */
const resetTheme = () => {
theme.value = null
localStorage.removeItem(THEME_KEY)
}
/** 初始化 */
onMounted(() => {
const stored = getStoredTheme()
theme.value = stored ?? null
applyTheme(theme.value)
// 监听系统主题变化
const handler = (e: MediaQueryListEvent) => {
if (theme.value === null) applyTheme(null) // 未手动设置时才响应系统变化
}
prefersDark.addEventListener('change', handler)
onBeforeUnmount(() => {
prefersDark.removeEventListener('change', handler)
})
})
// 响应式应用主题变化
watchEffect(() => {
applyTheme(theme.value)
})
return { theme, toggleTheme, resetTheme }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 三、ThemeToggle.vue(按钮组件)
<template>
<button @click="toggleTheme" class="p-2 rounded-xl border border-gray-400/30 hover:bg-gray-200 dark:hover:bg-gray-700 transition">
<span v-if="theme === 'dark'">🌙 暗色模式</span>
<span v-else-if="theme === 'light'">☀️ 亮色模式</span>
<span v-else>🪄 跟随系统</span>
</button>
<button @click="resetTheme" class="ml-2 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">重置</button>
</template>
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'
const { theme, toggleTheme, resetTheme } = useTheme()
</script>
<style scoped>
button {
cursor: pointer;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 四、main.ts(首屏防闪烁初始化)
在 main.ts 的最顶部加上:
// src/main.ts
// 防止闪烁:在 Vue 应用挂载前读取 localStorage 决定主题
(() => {
try {
const theme = localStorage.getItem('site-theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const el = document.documentElement
if (theme === 'dark' || (!theme && prefersDark)) {
el.setAttribute('data-theme', 'dark')
el.style.colorScheme = 'dark'
} else {
el.setAttribute('data-theme', 'light')
el.style.colorScheme = 'light'
}
} catch {}
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 五、CSS 变量定义
/* src/assets/theme.css */
:root {
--bg: #ffffff;
--text: #111827;
--muted: #6b7280;
color-scheme: light;
}
[data-theme="dark"] {
--bg: #0b1220;
--text: #e6eef6;
--muted: #9aa6b2;
color-scheme: dark;
}
body {
background: var(--bg);
color: var(--text);
transition: background-color 0.3s ease, color 0.3s ease;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然后在 main.ts 或 App.vue 引入:
import './assets/theme.css'
# 六、使用示例
<!-- App.vue -->
<template>
<main class="min-h-screen flex flex-col items-center justify-center">
<h1 class="text-3xl mb-4">Vue3 自动响应深色模式 🌗</h1>
<ThemeToggle />
</main>
</template>
<script setup lang="ts">
import ThemeToggle from '@/components/ThemeToggle.vue'
</script>
2
3
4
5
6
7
8
9
10
11
# 七、进阶可加分项(面试/项目中可讲)
- ✅ 使用
color-scheme保证系统控件(滚动条、表单)自适应。 - ✅ 深色/浅色 icon 用
currentColor填充或使用image-set() - ✅ 用 CSS variables + Tailwind 支持动态主题(可与 tailwind 的
darkMode: ['class', '[data-theme="dark"]']配合)。 - ✅ 可以封装成全局 store(如 Pinia)同步状态。