主题切换实现

主题切换实现

#resource / css #type / howto #status / evergreen

[!info] related notes


主题切换

1.在 :root 中定义主题变量,通过修改 document 控制生效的变量(CSS 变量 + data-theme 属性)

//style.css
:root[data-theme="dark"] {
--bg-color: #121212;
--text-color: #e0e0e0;
--surface-color: #1e1e1e;
--border-color: rgba(255, 255, 255, 0.12);
}

// 通过JavaScript切换类名
document.documentElement.setAttribute('data-theme', theme)

// 优点:
// 更好的性能,只需切换一个属性值
// 更清晰的主题定义,所有样式集中在CSS文件中
// 更容易调试和维护
// 浏览器原生支持的主题切换方式
// 支持热重载

// 缺点:
// 不能动态添加新的主题变量
// 主题配置不够灵活
// 不能在运行时修改具体的颜色值

2.在 store.ts 中存储主题及其样式,在用函数将所有样式应用到 document 中(JavaScript 动态设置样式变量)

Object.entries(themeStyle).forEach(([key, value]) => {
        document.documentElement.style.setProperty(`--${key}`, value)
})

完全动态,可以在运行时修改任何颜色值
可以动态添加新的主题
主题配置更灵活
可以保存用户自定义主题

性能较差,需要设置多个样式属性
可能造成样式闪烁
调试相对困难
主题定义分散在JS中

3.直接在每个 vue 组件下面用变量使用 css(组件级别的样式变量)

组件级别的精细控制
可以针对特定组件做特殊处理
更容易实现组件级别的主题切换

样式定义分散,难以统一管理
代码重复,每个组件都需要定义主题相关样式
维护成本高
不利于全局主题统一

4.使用 组件

5.动态加载CSS文件

通过动态创建或修改标签的href属性加载不同主题的CSS文件。

6. Vue/React框架方案

通过响应式数据绑定CSS变量

<script setup>
const theme = reactive({ color: 'red' });
</script>
<style scoped>
p { color: v-bind('theme.color'); }
</style>

7. 媒体查询(自动变化)

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;          /* 亮色主题文字颜色 */
    background-color: #ffffff;  /* 亮色主题背景色 */
  }
  /* ...其他亮色主题相关样式... */
}

SCSS

补充

可以结合第二种和第一种

:root {
  /* 默认主题变量 */
  --bg-color: #ffffff;
  --text-color: #000000;
}

/* 预定义主题 */
:root[data-theme="dark"] {
  --bg-color: #121212;
  --text-color: #e0e0e0;
}
export const useThemeStore = defineStore("theme", {
  state: () => ({
    currentTheme: "system",
    customThemes: {} as Record<string, ThemeStyle>,
  }),

  actions: {
    setTheme(theme: string) {
      // 设置预定义主题
      if (["light", "dark", "system"].includes(theme)) {
        document.documentElement.setAttribute("data-theme", theme);
      }
      // 应用自定义主题
      else if (this.customThemes[theme]) {
        Object.entries(this.customThemes[theme]).forEach(([key, value]) => {
          document.documentElement.style.setProperty(`--${key}`, value);
        });
      }
      this.currentTheme = theme;
    },

    applyThemeSystem() {
      const prefersDark = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches;
      this.themeMode = prefersDark ? "dark" : "light";
      this.applyTheme();
    },
  },
});

本次实现方法

结合第三种和第四种

使用 vuetify 控制组件主题
对于自定义的组件,在 组件中使用 vuetify 的变量方式接入 vuetify 中

vuetify theme 的属性

color: #c4c4c4;暗灰色 font: ‘#d4d4d4’

// dark
{
    "background": "#121212",
    "surface": "#212121",
    "surface-bright": "#ccbfd6",
    "surface-light": "#424242",
    "surface-variant": "#a3a3a3",
    "on-surface-variant": "#424242",
    "primary": "#2196F3",
    "primary-darken-1": "#277CC1",
    "secondary": "#424242",
    "secondary-darken-1": "#48A9A6",
    "error": "#FF5252",
    "info": "#2196F3",
    "success": "#4CAF50",
    "warning": "#FB8C00",
    "accent": "#FF4081",
    "custom-color": "#BB86FC",
    "sidebar-bg": "#1E1E1E",
    "toolbar-bg": "#2D2D2D",
    "on-background": "#fff",
    "on-surface": "#fff",
    "on-surface-bright": "#000",
    "on-surface-light": "#fff",
    "on-primary": "#fff",
    "on-primary-darken-1": "#fff",
    "on-secondary": "#fff",
    "on-secondary-darken-1": "#fff",
    "on-error": "#fff",
    "on-info": "#fff",
    "on-success": "#fff",
    "on-warning": "#fff",
    "on-accent": "#fff",
    "on-custom-color": "#fff",
    "on-sidebar-bg": "#fff",
    "on-toolbar-bg": "#fff"
}
    color-scheme: dark;
    --v-theme-background: 18,18,18;
    --v-theme-background-overlay-multiplier: 1;
    --v-theme-surface: 33,33,33;
    --v-theme-surface-overlay-multiplier: 1;
    --v-theme-surface-bright: 204,191,214;
    --v-theme-surface-bright-overlay-multiplier: 2;
    --v-theme-surface-light: 66,66,66;
    --v-theme-surface-light-overlay-multiplier: 1;
    --v-theme-surface-variant: 255,255,255;
    --v-theme-surface-variant-overlay-multiplier: 2;
    --v-theme-on-surface-variant: 255,255,255;
    --v-theme-primary: 33,150,243;
    --v-theme-primary-overlay-multiplier: 2;
    --v-theme-primary-darken-1: 39,124,193;
    --v-theme-primary-darken-1-overlay-multiplier: 2;
    --v-theme-secondary: 66,66,66;
    --v-theme-secondary-overlay-multiplier: 1;
    --v-theme-secondary-darken-1: 72,169,166;
    --v-theme-secondary-darken-1-overlay-multiplier: 2;
    --v-theme-error: 255,82,82;
    --v-theme-error-overlay-multiplier: 2;
    --v-theme-info: 33,150,243;
    --v-theme-info-overlay-multiplier: 2;
    --v-theme-success: 76,175,80;
    --v-theme-success-overlay-multiplier: 2;
    --v-theme-warning: 251,140,0;
    --v-theme-warning-overlay-multiplier: 2;
    --v-theme-accent: 255,64,129;
    --v-theme-accent-overlay-multiplier: 2;
    --v-theme-scrollbar-track: 204,191,214;
    --v-theme-scrollbar-track-overlay-multiplier: 2;
    --v-theme-scrollbar-thumb: 255,255,255;
    --v-theme-scrollbar-thumb-overlay-multiplier: 2;
    --v-theme-scrollbar-thumb-hover: 255,255,255;
    --v-theme-scrollbar-thumb-hover-overlay-multiplier: 2;
    --v-theme-on-background: 255,255,255;
    --v-theme-on-surface: 255,255,255;
    --v-theme-on-surface-bright: 0,0,0;
    --v-theme-on-surface-light: 255,255,255;
    --v-theme-on-primary: 255,255,255;
    --v-theme-on-primary-darken-1: 255,255,255;
    --v-theme-on-secondary: 255,255,255;
    --v-theme-on-secondary-darken-1: 255,255,255;
    --v-theme-on-error: 255,255,255;
    --v-theme-on-info: 255,255,255;
    --v-theme-on-success: 255,255,255;
    --v-theme-on-warning: 255,255,255;
    --v-theme-on-accent: 255,255,255;
    --v-theme-on-scrollbar-track: 0,0,0;
    --v-theme-on-scrollbar-thumb: 0,0,0;
    --v-theme-on-scrollbar-thumb-hover: 0,0,0;
    --v-border-color: 255, 255, 255;
    --v-border-opacity: 0.12;
    --v-high-emphasis-opacity: 1;
    --v-medium-emphasis-opacity: 0.7;
    --v-disabled-opacity: 0.5;
    --v-idle-opacity: 0.1;
    --v-hover-opacity: 0.04;
    --v-focus-opacity: 0.12;
    --v-selected-opacity: 0.08;
    --v-activated-opacity: 0.12;
    --v-pressed-opacity: 0.16;
    --v-dragged-opacity: 0.08;
    --v-theme-kbd: 33, 37, 41;
    --v-theme-on-kbd: 255, 255, 255;
    --v-theme-code: 52, 52, 52;
    --v-theme-on-code: 204, 204, 204;
// light
{
    "background": "#FFFFFF",
    "surface": "#FFFFFF",
    "surface-bright": "#FFFFFF",
    "surface-light": "#EEEEEE",
    "surface-variant": "#424242",
    "on-surface-variant": "#EEEEEE",
    "primary": "#1867C0",
    "primary-darken-1": "#1F5592",
    "secondary": "#5CBBF6",
    "secondary-darken-1": "#018786",
    "error": "#FF5252",
    "info": "#2196F3",
    "success": "#4CAF50",
    "warning": "#FFC107",
    "accent": "#4CAF50",
    "custom-color": "#FF00FF",
    "sidebar-bg": "#f5f5f5",
    "toolbar-bg": "#ffffff",
    "on-background": "#000",
    "on-surface": "#000",
    "on-surface-bright": "#000",
    "on-surface-light": "#000",
    "on-primary": "#fff",
    "on-primary-darken-1": "#fff",
    "on-secondary": "#000",
    "on-secondary-darken-1": "#fff",
    "on-error": "#fff",
    "on-info": "#fff",
    "on-success": "#fff",
    "on-warning": "#000",
    "on-accent": "#fff",
    "on-custom-color": "#fff",
    "on-sidebar-bg": "#000",
    "on-toolbar-bg": "#000"
}

主题设置模块

通过设置组件得到用户设置的 themeMode:string
再使用 themeStore 来处理

创建于 2025/1/1 更新于 2026/5/27