深色亮色主题切换 CSS 变量方案
深色亮色主题切换CSS变量方案
引言
深色模式(Dark Mode)已成为现代应用的标配功能。从 macOS 到 iOS,从 Chrome 到 VS Code,深色模式无处不在。在企业级后台管理系统中,支持深色/亮色主题切换不仅是用户体验的需求,更是长时间使用场景下保护视力的刚需。
本文分享基于 CSS 变量实现深色/亮色主题切换的完整方案,涵盖 50+ 主题配置项的设计、用户级持久化存储、主题配置的导入导出,以及与 Element Plus 组件库的深度适配。
核心内容
一、主题切换方案对比
flowchart TB
A[主题切换方案] --> B[多套CSS文件]
A --> C[CSS变量方案]
A --> D[CSS-in-JS方案]
B --> B1[优点:兼容性好]
B --> B2[缺点:维护成本高
切换需重新加载]
C --> C1[优点:切换无闪烁
维护简单
性能好]
C --> C2[缺点:IE不支持
需规范变量命名]
D --> D1[优点:动态性强]
D --> D2[缺点:运行时开销
与UI库集成复杂]
style C fill:#4CAF50,color:#fff
CSS 变量方案是当前最佳选择:切换无需重新加载样式文件、与 Element Plus 原生支持 CSS 变量完美契合、性能开销几乎为零。
二、CSS 变量体系设计
2.1 变量分类与命名规范
flowchart TB
A[主题变量体系] --> B[基础颜色]
A --> C[功能颜色]
A --> D[文本颜色]
A --> E[边框颜色]
A --> F[背景颜色]
A --> G[阴影]
A --> H[间距与圆角]
B --> B1[--el-color-primary]
B --> B2[--el-color-success]
B --> B3[--el-color-warning]
B --> B4[--el-color-danger]
B --> B5[--el-color-info]
C --> C1[--el-color-primary-light-3]
C --> C2[--el-color-primary-light-5]
C --> C3[--el-color-primary-light-7]
C --> C4[--el-color-primary-light-9]
C --> C5[--el-color-primary-dark-2]
D --> D1[--el-text-color-primary]
D --> D2[--el-text-color-regular]
D --> D3[--el-text-color-secondary]
D --> D4[--el-text-color-placeholder]
D --> D5[--el-text-color-disabled]
E --> E1[--el-border-color]
E --> E2[--el-border-color-light]
E --> E3[--el-border-color-lighter]
E --> E4[--el-border-color-extra-light]
F --> F1[--el-bg-color]
F --> F2[--el-bg-color-page]
F --> F3[--el-bg-color-overlay]
F --> F4[--el-fill-color]
F --> F5[--el-fill-color-light]
F --> F6[--el-fill-color-lighter]
2.2 亮色主题变量定义
/* styles/theme/light.css */
:root,
html[data-theme="light"] {
/* ===== 基础颜色 ===== */
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
/* ===== Primary 渐变色 ===== */
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
/* ===== 文本颜色 ===== */
--el-text-color-primary: #303133;
--el-text-color-regular: #606266;
--el-text-color-secondary: #909399;
--el-text-color-placeholder: #a8abb2;
--el-text-color-disabled: #c0c4cc;
/* ===== 边框颜色 ===== */
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f6fc;
/* ===== 背景颜色 ===== */
--el-bg-color: #ffffff;
--el-bg-color-page: #f2f3f5;
--el-bg-color-overlay: #ffffff;
/* ===== 填充颜色 ===== */
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-blank: #ffffff;
/* ===== 阴影 ===== */
--el-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
--el-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
--el-box-shadow-darker: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
/* ===== 自定义扩展变量 ===== */
--header-bg-color: #ffffff;
--sidebar-bg-color: #ffffff;
--menu-active-bg-color: #ecf5ff;
--menu-hover-bg-color: #f5f7fa;
--logo-text-color: #303133;
--tabs-bg-color: #ffffff;
--card-bg-color: #ffffff;
}
2.3 深色主题变量定义
/* styles/theme/dark.css */
html[data-theme="dark"] {
/* ===== 基础颜色 ===== */
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
/* ===== Primary 渐变色(深色模式下需要特殊处理) ===== */
--el-color-primary-light-3: #3375b9;
--el-color-primary-light-5: #2a598a;
--el-color-primary-light-7: #213d5b;
--el-color-primary-light-9: #18212d;
--el-color-primary-dark-2: #66b1ff;
/* ===== 文本颜色(深色模式反转) ===== */
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
/* ===== 边框颜色 ===== */
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
/* ===== 背景颜色 ===== */
--el-bg-color: #141414;
--el-bg-color-page: #0a0a0a;
--el-bg-color-overlay: #1d1e1f;
/* ===== 填充颜色 ===== */
--el-fill-color: #303030;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--el-fill-color-blank: transparent;
/* ===== 阴影(深色模式使用更深的阴影) ===== */
--el-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), 0 0 6px rgba(0, 0, 0, 0.3);
--el-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.6);
--el-box-shadow-darker: 0 2px 4px rgba(0, 0, 0, 0.6), 0 0 6px rgba(0, 0, 0, 0.4);
/* ===== 自定义扩展变量 ===== */
--header-bg-color: #1d1e1f;
--sidebar-bg-color: #141414;
--menu-active-bg-color: #18212d;
--menu-hover-bg-color: #262727;
--logo-text-color: #e5eaf3;
--tabs-bg-color: #1d1e1f;
--card-bg-color: #1d1e1f;
}
三、主题切换核心实现
3.1 主题状态管理
/**
* 主题状态管理
* 管理深色/亮色模式切换、主题色配置等
*/
import { defineStore } from 'pinia'
export const useThemeStore = defineStore('theme', {
state: () => ({
/** 当前主题模式:light / dark / auto */
themeMode: 'light',
/** 主题色 */
primaryColor: '#409eff',
/** 是否开启灰色模式(用于哀悼日等场景) */
isGrey: false,
/** 是否开启色弱模式 */
isWeak: false,
/** 自定义主题配置(覆盖默认变量) */
customVars: {},
}),
getters: {
/** 实际生效的主题(auto模式根据系统偏好决定) */
effectiveTheme: (state) => {
if (state.themeMode === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
return state.themeMode
},
/** 是否为深色模式 */
isDark: (state) => state.effectiveTheme === 'dark',
},
actions: {
/** 切换主题模式 */
setThemeMode(mode) {
this.themeMode = mode
this.applyTheme()
this.persistConfig()
},
/** 设置主题色 */
setPrimaryColor(color) {
this.primaryColor = color
this.applyPrimaryColor()
this.persistConfig()
},
/** 应用主题到 DOM */
applyTheme() {
const html = document.documentElement
html.setAttribute('data-theme', this.effectiveTheme)
// 深色模式下为 body 添加 class
if (this.isDark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
},
/** 应用主题色(动态计算渐变色) */
applyPrimaryColor() {
const el = document.documentElement
el.style.setProperty('--el-color-primary', this.primaryColor)
// 自动计算 primary 的 light 和 dark 变体
const lightColors = generateLightColors(this.primaryColor)
lightColors.forEach(({ level, color }) => {
el.style.setProperty(`--el-color-primary-light-${level}`, color)
})
const darkColor = darkenColor(this.primaryColor, 0.2)
el.style.setProperty('--el-color-primary-dark-2', darkColor)
},
/** 应用灰色模式 */
applyGrey() {
document.body.style.filter = this.isGrey ? 'grayscale(1)' : ''
},
/** 应用色弱模式 */
applyWeak() {
document.body.style.filter = this.isWeak ? 'invert(0.8)' : ''
},
/** 持久化配置 */
persistConfig() {
const config = {
themeMode: this.themeMode,
primaryColor: this.primaryColor,
isGrey: this.isGrey,
isWeak: this.isWeak,
customVars: this.customVars,
}
localStorage.setItem('theme-config', JSON.stringify(config))
},
/** 恢复配置 */
restoreConfig() {
try {
const config = JSON.parse(localStorage.getItem('theme-config'))
if (config) {
this.themeMode = config.themeMode || 'light'
this.primaryColor = config.primaryColor || '#409eff'
this.isGrey = config.isGrey || false
this.isWeak = config.isWeak || false
this.customVars = config.customVars || {}
}
} catch (e) {
console.warn('恢复主题配置失败', e)
}
},
/** 初始化主题 */
initTheme() {
this.restoreConfig()
this.applyTheme()
this.applyPrimaryColor()
this.applyGrey()
this.applyWeak()
// 监听系统主题变化(auto模式)
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (this.themeMode === 'auto') {
this.applyTheme()
}
})
},
},
})
3.2 颜色工具函数
/**
* 颜色工具函数
* 用于主题色的渐变计算
*/
/**
* 将 HEX 颜色转为 RGB
*/
function hexToRgb(hex) {
const result = /^#?([a-fd]{2})([a-fd]{2})([a-fd]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null
}
/**
* 生成主题色的 light 变体
* 深色模式下混合黑色,亮色模式下混合白色
*/
function generateLightColors(primary, isDark = false) {
const levels = [3, 5, 7, 9]
const rgb = hexToRgb(primary)
if (!rgb) return []
return levels.map(level => {
const mixRatio = level / 10
const mixColor = isDark ? { r: 0, g: 0, b: 0 } : { r: 255, g: 255, b: 255 }
const r = Math.round(rgb.r * (1 - mixRatio) + mixColor.r * mixRatio)
const g = Math.round(rgb.g * (1 - mixRatio) + mixColor.g * mixRatio)
const b = Math.round(rgb.b * (1 - mixRatio) + mixColor.b * mixRatio)
return {
level,
color: `rgb(${r}, ${g}, ${b})`,
}
})
}
/**
* 加深颜色
*/
function darkenColor(hex, amount) {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const r = Math.max(0, Math.round(rgb.r * (1 - amount)))
const g = Math.max(0, Math.round(rgb.g * (1 - amount)))
const b = Math.max(0, Math.round(rgb.b * (1 - amount)))
return `rgb(${r}, ${g}, ${b})`
}
四、主题切换组件
<template>
<div class="theme-switcher">
<!-- 深色/亮色切换 -->
<el-switch
v-model="isDark"
:active-icon="Moon"
:inactive-icon="Sunny"
inline-prompt
@change="onThemeChange"
/>
<!-- 更多主题设置 -->
<el-popover placement="bottom-end" :width="320" trigger="click">
<template #reference>
<el-button :icon="Setting" circle />
</template>
<div class="theme-settings">
<!-- 主题模式 -->
<div class="setting-item">
<span>主题模式</span>
<el-radio-group v-model="themeStore.themeMode" size="small" @change="onModeChange">
<el-radio-button value="light">亮色</el-radio-button>
<el-radio-button value="dark">深色</el-radio-button>
<el-radio-button value="auto">跟随系统</el-radio-button>
</el-radio-group>
</div>
<!-- 主题色 -->
<div class="setting-item">
<span>主题色</span>
<div class="color-picker-group">
<div
v-for="color in presetColors"
:key="color"
class="color-dot"
:class="{ active: themeStore.primaryColor === color }"
:style="{ backgroundColor: color }"
@click="themeStore.setPrimaryColor(color)"
/>
<el-color-picker
v-model="customColor"
size="small"
@change="themeStore.setPrimaryColor"
/>
</div>
</div>
<!-- 灰色模式 -->
<div class="setting-item">
<span>灰色模式</span>
<el-switch v-model="themeStore.isGrey" @change="themeStore.applyGrey()" />
</div>
<!-- 色弱模式 -->
<div class="setting-item">
<span>色弱模式</span>
<el-switch v-model="themeStore.isWeak" @change="themeStore.applyWeak()" />
</div>
<!-- 导入导出 -->
<div class="setting-item">
<span>配置管理</span>
<div>
<el-button size="small" @click="exportTheme">导出</el-button>
<el-button size="small" @click="importTheme">导入</el-button>
</div>
</div>
</div>
</el-popover>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Moon, Sunny, Setting } from '@element-plus/icons-vue'
import { useThemeStore } from '@/store/modules/theme'
const themeStore = useThemeStore()
const isDark = computed({
get: () => themeStore.isDark,
set: (val) => themeStore.setThemeMode(val ? 'dark' : 'light'),
})
const customColor = ref(themeStore.primaryColor)
const presetColors = [
'#409eff', '#009688', '#536dfe',
'#ff5c93', '#c62f2f', '#fd726d',
'#f25d43', '#e6a23c', '#67c23a',
]
const onThemeChange = (dark) => {
themeStore.setThemeMode(dark ? 'dark' : 'light')
}
const onModeChange = (mode) => {
themeStore.setThemeMode(mode)
}
/** 导出主题配置 */
const exportTheme = () => {
const config = {
themeMode: themeStore.themeMode,
primaryColor: themeStore.primaryColor,
customVars: themeStore.customVars,
exportTime: new Date().toISOString(),
}
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `theme-config-${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
}
/** 导入主题配置 */
const importTheme = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (ev) => {
try {
const config = JSON.parse(ev.target.result)
if (config.themeMode) themeStore.setThemeMode(config.themeMode)
if (config.primaryColor) themeStore.setPrimaryColor(config.primaryColor)
if (config.customVars) themeStore.customVars = config.customVars
} catch {
console.warn('导入主题配置失败:文件格式错误')
}
}
reader.readAsText(file)
}
input.click()
}
</script>
五、50+ 主题配置项清单
flowchart LR
A[50+ 主题配置项] --> B[Element Plus 内置
约30项]
A --> C[布局扩展
约10项]
A --> D[业务扩展
约10+项]
B --> B1[5种基础色]
B --> B2[4级文本色]
B --> B3[4级边框色]
B --> B4[3级背景色]
B --> B5[5级填充色]
B --> B6[3级阴影]
B --> B7[圆角/字体等]
C --> C1[头部背景色]
C --> C2[侧边栏背景色]
C --> C3[菜单激活色]
C --> C4[菜单悬停色]
C --> C5[Logo文字色]
C --> C6[标签页背景色]
C --> C7[卡片背景色]
D --> D1[图表配色]
D --> D2[自定义组件色]
D --> D3[数据高亮色]
D --> D4[状态指示色]
六、深色模式适配要点
6.1 图片适配
/* 深色模式下图片降低亮度 */
html[data-theme="dark"] img {
opacity: 0.8;
transition: opacity 0.3s;
}
html[data-theme="dark"] img:hover {
opacity: 1;
}
6.2 自定义组件适配
/* 自定义卡片组件适配深色模式 */
.custom-card {
background-color: var(--card-bg-color, var(--el-bg-color-overlay));
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-light);
color: var(--el-text-color-primary);
transition: all 0.3s;
}
6.3 ECharts 图表适配
/**
* ECharts 深色模式适配
* 根据当前主题动态调整图表配色
*/
function getChartTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
return {
color: isDark
? ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de']
: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'],
backgroundColor: 'transparent',
textStyle: {
color: isDark ? '#cfd3dc' : '#606266',
},
legend: {
textStyle: {
color: isDark ? '#cfd3dc' : '#606266',
},
},
categoryAxis: {
axisLine: { lineStyle: { color: isDark ? '#4c4d4f' : '#dcdfe6' } },
axisLabel: { color: isDark ? '#a3a6ad' : '#909399' },
splitLine: { lineStyle: { color: isDark ? '#363637' : '#ebeef5' } },
},
}
}
七、切换流程
sequenceDiagram
participant U as 用户
participant S as ThemeStore
participant D as DOM
participant L as localStorage
U->>S: 切换主题模式
S->>S: 更新 state
S->>D: setAttribute('data-theme', 'dark')
S->>D: body.classList.add('dark')
S->>D: CSS 变量自动切换
S->>L: 持久化配置
Note over D: 浏览器自动应用<br/>对应主题的 CSS 变量
U->>S: 修改主题色
S->>D: setProperty('--el-color-primary', '#409eff')
S->>S: 计算渐变色
S->>D: 设置 light-3/5/7/9 变体
S->>L: 持久化配置
结论与建议
核心要点
- CSS 变量是主题切换的最佳方案:零性能开销、切换无闪烁、与 UI 库深度集成
- 变量体系要分层设计:基础色 → 功能色 → 扩展色,层次清晰便于维护
- 深色模式不是简单反色:需要专门调校每个变量的值,确保对比度和可读性
- 用户级持久化:主题偏好保存到服务端,换设备也能恢复
最佳实践建议
| 建议 | 说明 |
|---|---|
| 避免硬编码颜色 | 所有颜色必须使用 CSS 变量,不要在组件中写死颜色值 |
| 深色模式单独调校 | 不要用算法自动反色,每个变量值都需要人工调校 |
| 过渡动画 | 主题切换时添加 transition: all 0.3s 避免闪烁 |
| 图片处理 | 深色模式下降低图片亮度或使用深色版本图片 |
| 图表适配 | ECharts 等图表库需要单独适配深色主题 |
| 导入导出 | 支持主题配置导入导出,方便团队统一风格 |
| 系统跟随 | 支持 prefers-color-scheme 自动跟随系统主题 |