前端主题切换方案

前端主题切换方案


主题切换 css 前端

其做法就是提前准备好几套 CSS 主题样式文件,在需要的时候,创建 link 标签动态加载到 head 标签中,或者是动态改变 link 标签的 href 属性。

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
      }
    </style>
    <link rel="stylesheet" href="./style1.css" />
  </head>
  <body>
    <p>主题切换</p>
    <p>方案1:link标签动态引入</p>
    <button onclick="changeTheme(1)">style1</button>
    <button onclick="changeTheme(2)">style2</button>
  </body>
  <script>
    function changeTheme(type) {
      document.head.querySelector("link").href = `./style${type}.css`;
    }
  </script>
</html>
// style1.css
body {
  background-color: #fff;
}
p {
  color: #333;
}
// style2.css
body {
  background-color: rgba(0, 0, 0, 0.6);
}
p {
  color: #fff;
}

优点:

  • 实现了按需加载,提高了首屏加载时的性能

缺点:

  • 动态加载样式文件,如果文件过大网络情况不佳的情况下可能会有加载延迟,导致样式切换不流畅
  • 如果主题样式表内定义不当,会有优先级问题
  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

方案 2:提前引入所有主题样式,做类名切换

这种方案与第一种比较类似,为了解决反复加载样式文件问题提前将样式全部引入,在需要切换主题的时候将指定的根元素类名更换,相当于直接做了样式覆盖,在该类名下的各个样式就统一地更换了。

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
      }
    </style>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <p>主题切换</p>
    <p>方案2:提前引入所有主题样式,做类名切换</p>
    <button onclick="changeTheme('day')">style1</button>
    <button onclick="changeTheme('dark')">style2</button>
  </body>
  <script>
    function changeTheme(type) {
      document.documentElement.className = type;
    }
  </script>
</html>
body.day {
  background-color: #fff;
  color: #333;
}
body.dark {
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
}

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源
  • 如果主题样式表内定义不当,同样会有优先级问题
  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

方案 3:CSS 变量+类名切换(推荐)

大体思路跟方案 2 相似,依然是提前将样式文件载入,切换时将指定的根元素类名更换。 不过这里相对灵活的是,默认在根作用域下定义好 CSS 变量,只需要在不同的主题下更改 CSS 变量对应的取值即可。

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
      }
    </style>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <p>主题切换</p>
    <p>方案3:CSS变量+类名切换</p>
    <button onclick="changeTheme('day')">style1</button>
    <button onclick="changeTheme('dark')">style2</button>
  </body>
  <script>
    function changeTheme(type) {
      document.documentElement.className = type;
    }
  </script>
</html>
html.dark {
  /* https://segmentfault.com/a/1190000042184163 */
  /* color-scheme属性 可以更改浏览器的默认配色方案*/
  /* 可选值:normal light dark */
  /* color-scheme 的作用范围有限,包括表单控件、滚动条和 CSS 系统颜色的使用值 */
  /* 表单控件:比如checkbox */
  /* 系统颜色指的是浏览器内置的一些颜色。比如button,它能自动渲染成深色模式,除了因为它是表单元素外,最根本的一点是这些表单元素的默认样式上使用了系统颜色。 */
  /*
    button{
      color: buttontext;
      background-color: buttonface;
      border-color: buttonborder;
    }
    这里的buttontext、buttonface、buttonborder就是系统颜色了,它会根据color-scheme自动适应深色模式
  */
  /*
  这里加color-scheme只是为了适配下滚动条的颜色,与该主题切换方案本身没有关联
  */
  color-scheme: dark;
}

/* 定义根作用域下的变量 充当默认值*/
:root {
  --theme-color: blue;
  --theme-background: #eee;
}

/* 更改dark类名下变量的取值 */
.dark {
  --theme-color: #fff;
  --theme-background: rgba(0, 0, 0, 0.6);
}

.day {
  --theme-color: #333;
  --theme-background: #fff;
}

html {
  color: var(--theme-color);
  background: var(--theme-background);
}

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 在需要切换主题的地方利用 var()绑定变量即可,不存在优先级问题
  • 新增或修改主题方便灵活,仅需新增或修改 CSS 变量即可,在 var()绑定样式变量的地方就会自动更换

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

方案 4:

Vue3 新特性: style 标签内使用 v-bind

基础用法

<script setup>
  const theme = {
    color: 'red'
  }
</script>

<template>
<p>hello</p>
</template>

<style scoped>
  p {
    color: v-bind('theme.color');
  }
</style>

Vue3中在style样式通过v-bind()绑定变量的原理其实就是给元素绑定 CSS 变量,在绑定的数据更新时调用CSSStyleDeclaration.setProperty更新 CSS 变量值。

注意

这个方式只能在单文件组件.vue中使用, 内部使用的变量会挂在该组件的最外层元素上.

封装一个 hook

前面方案 3 基于 CSS 变量绑定样式是在:root上定义变量,然后在各个地方都可以获取到根元素上定义的变量。在当前方案下,我们可以利用 VuexPinia 对全局样式变量做统一管理,或者等装一个 hook:

// /hooks/useTheme/theme_day.ts
export default {
  color: "#333",
  background: "#FFF",
};
// /hooks/useTheme/theme_dark.ts
export default {
  color: "#FFF",
  background: "rgba(0,0,0,0.6)",
};
// /hooks/useTheme/index.ts
import { shallowRef } from "vue";
// 引入主题
import theme_day from "./theme_day";
import theme_dark from "./theme_dark";

// 定义在全局的样式变量
const theme = shallowRef({});

export function useTheme() {
  theme.value = theme_day;

  const setDayTheme = () => {
    theme.value = theme_day;
  };

  const setDarkTheme = () => {
    theme.value = theme_dark;
  };

  return {
    theme,
    setDayTheme,
    setDarkTheme,
  };
}
// components/MyButton.vue
<script setup lang="ts">
import { useTheme } from "../hooks/useTheme";

const { setDarkTheme, setDayTheme } = useTheme();

const change2Dark = () => {
  setDarkTheme();
};

const change2Day = () => {
  setDayTheme();
};
</script>

<template>
  <div>
    <button @click="change2Dark">dark</button>
    <button @click="change2Day">day</button>
  </div>
</template>
// pages/index.vue
<script setup lang="ts">
import { useTheme } from "../hooks/useTheme";
import MyButton from "../components/MyButton.vue";

const { theme } = useTheme();
</script>

<template>
  <div>
    <div class="box">
      <p>test</p>
    </div>
    <my-button />
  </div>
</template>

<style>
.box {
  width: 100px;
  height: 100px;
  background: v-bind("theme.background");
  color: v-bind("theme.color");
}
</style>

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 在需要切换主题的地方利用 v-bind 绑定变量即可,不存在优先级问题
  • 新增或修改主题方便灵活,仅需新增或修改 JS 变量即可,在 v-bind()绑定样式变量的地方就会自动更换

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源
  • 这种方式只要是在组件上绑定了动态样式的地方都会有对应的编译成哈希化的 CSS 变量,而不像方案 3 统一地就在:root 上设置(不确定在达到一定量级以后的性能),也可能正是如此,Vue 官方也并未采用此方式做全站的主题切换

方案 5:SCSS + mixin + 类名切换

主要是运用 SCSS 的混合+CSS 类名切换,其原理主要是将使用到 mixin 混合的地方编译为固定的 CSS 以后,再通过类名切换去做样式的覆盖.

定义 SCSS 变量:

$font_samll: 12Px;
$font_medium: 14Px;
$font_large: 18Px;

$background-color-theme: #d43c33; //背景主题颜色默认
$background-color-theme1: #42b983; //背景主题颜色1
$background-color-theme2: #333; //背景主题颜色2

$font-color-theme: #fff; //字体主题颜色默认
$font-color-theme1: #fff; //字体主题颜色1
$font-color-theme2: #ddd; //字体主题颜色2

定义混合 mixin:

@import "./variable.scss";

@mixin bg_color(){
  background: $background-color-theme;
  [data-theme=theme1] & {
    background: $background-color-theme1;
  }
  [data-theme=theme2] & {
    background: $background-color-theme2;
  }
}

@mixin font_color(){
  color: $font-color-theme;
  [data-theme=theme1] & {
    color: $font-color-theme1;
  }
  [data-theme=theme2] & {
    color: $font-color-theme2;
  }
}

在页面中使用

<template>
  <div class="box">
    <p>test</p>
  </div>
  <button @click="changeTheme()">theme</button>
  <button @click="changeTheme(1)">theme1</button>
  <button @click="changeTheme(2)">theme2</button>
</template>

<script>
  export default {
    name: "Header",
    methods: {
      changeTheme(type = "") {
        document.documentElement.setAttribute("data-theme", `theme${type}`);
      },
    },
  };
</script>

<style scoped lang="scss">
  @import "../assets/css/variable";
  @import "../assets/css/mixin";
  .box {
    width: 100px;
    height: 100px;
    font-size: $font_medium;
    @include bg_color();
    @include font_color();
  }
</style>

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 在需要切换主题的地方利用 mixin 混合绑定变量即可,不存在优先级问题
  • 新增或修改主题方便灵活,仅需新增或修改 SCSS 变量即可,经过编译后会将所有主题全部编译出来

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

方案 6:CSS 变量+动态 setProperty

此方案较于前几种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前几种方案更适用于定义预设的几种主题。

主要实现思路: 只需在全局中设置好预设的全局 CSS 变量样式,无需单独为每一个主题类名下重新设定 CSS 变量值,因为主题是由用户动态决定。

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        color: var(--theme-color);
        background: var(--theme-background);
      }
    </style>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <p>主题切换</p>
    <p>方案6:</p>
    <button onclick="changeTheme('day')">style1</button>
    <button onclick="changeTheme('dark')">style2</button>
  </body>
  <script>
    function setCssVar(prop, val, dom = document.documentElement) {
      dom.style.setProperty(prop, val);
    }
    function changeTheme(type) {
      if (type === "day") {
        setCssVar("--theme-color", "#333");
        setCssVar("--theme-background", "#FFF");
      }
      if (type === "dark") {
        setCssVar("--theme-color", "#FFF");
        setCssVar("--theme-background", "rgba(0,0,0,0.6)");
      }
    }
  </script>
</html>
// style.css
:root {
  --theme-color: #333;
  --theme-background: #fff;
}

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 其原理跟方案 4 利用 Vue3 的新特性 v-bind 实际上是一致的,只不过此方案只在:root 上动态更改 CSS 变量而 Vue3 中会将 CSS 变量绑定到任何依赖该变量的节点上。
  • 需要切换主题的地方只用在:root 上动态更改 CSS 变量值即可,不存在优先级问题
  • 新增或修改主题方便灵活

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源(相对于前几种预设好的主题,这种方式的样式定义在首屏加载基本可以忽略不计)

参考文章

https://juejin.cn/post/7134594122391748615

© 2025 Niko Xie