Glittering's blog Glittering's blog
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • CSS
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)

Glitz Ma

前端开发工程师
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • CSS
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)
  • CSS

    • CSS教程和技巧收藏
    • css块元素和行内元素
    • 盒子模型
    • BFC和IFC
    • 字体font-weight相关知识
    • CSS-function汇总
    • CSS3之has函数的使用
    • CSS3之transition过渡
    • CSS3之animation动画
    • css动画性能优化
    • flex布局语法
    • flex布局案例
    • Grid布局语法
    • flex布局和grid布局的区别
    • 「布局技巧」图片未加载前自动撑开元素高度
    • 文字在一行或多行时超出显示省略号
    • 水平垂直居中的几种方式-案例
    • 如何根据系统主题自动响应CSS深色模式
      • 一、原理(一句话)
      • 二、CSS-only(最简单)
      • 三、推荐做法:CSS 变量 + data-theme(支持自动、手动与持久化)
        • CSS(变量与 class/data-attribute)
        • JS:初始化、切换、持久化、监听系统变更
      • 四、避免闪烁(FOUC / 抖动)——服务端渲染 / 初始渲染注意
      • 五、扩展与兼容性注意点
      • 六、面试中可说的精炼回答(30s)
      • 七、在vue3+Composition API中应用
        • 一、目录结构建议
        • 二、useTheme.ts(核心逻辑)
        • 三、ThemeToggle.vue(按钮组件)
        • 四、main.ts(首屏防闪烁初始化)
        • 五、CSS 变量定义
        • 六、使用示例
        • 七、进阶可加分项(面试/项目中可讲)
    • 工作中遇到的css问题记录
    • 今天总结一下用到的css吧
  • 页面
  • CSS
mamingjuan
2020-03-31
目录

如何根据系统主题自动响应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); }
1
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);
}
1
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);
    }
  });
}
1
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>
1
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
1
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 }
}
1
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>
1
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 {}
})()
1
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;
}
1
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'
1

# 六、使用示例

<!-- 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>
1
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)同步状态。

#css
上次更新: 2025/11/25, 03:24:47
水平垂直居中的几种方式-案例
工作中遇到的css问题记录

← 水平垂直居中的几种方式-案例 工作中遇到的css问题记录→

Copyright © 2015-2025 Glitz Ma
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式