深色亮色主题切换 CSS 变量方案

作者:忆笙智云官方 | 发布时间:2026-06-10 16:30 | 更新时间:2026-07-10 16:30

深色亮色主题切换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: 持久化配置

结论与建议

核心要点

  1. CSS 变量是主题切换的最佳方案:零性能开销、切换无闪烁、与 UI 库深度集成
  2. 变量体系要分层设计:基础色 → 功能色 → 扩展色,层次清晰便于维护
  3. 深色模式不是简单反色:需要专门调校每个变量的值,确保对比度和可读性
  4. 用户级持久化:主题偏好保存到服务端,换设备也能恢复

最佳实践建议

建议 说明
避免硬编码颜色 所有颜色必须使用 CSS 变量,不要在组件中写死颜色值
深色模式单独调校 不要用算法自动反色,每个变量值都需要人工调校
过渡动画 主题切换时添加 transition: all 0.3s 避免闪烁
图片处理 深色模式下降低图片亮度或使用深色版本图片
图表适配 ECharts 等图表库需要单独适配深色主题
导入导出 支持主题配置导入导出,方便团队统一风格
系统跟随 支持 prefers-color-scheme 自动跟随系统主题

相关资源