一、前言
平时做 ToB 或 ToC 项目,最怕什么?最怕产品经理突然跑过来:"我们要给客户做定制化,换个品牌色",或者用户反馈"晚上太亮了,能不能加个暗黑模式?"
如果你以前是硬编码颜色,比如满屏的 bg-blue-500,那改起来简直酸爽,全局搜索替换还得怕漏了某个角落。

今天分享一套我目前在用的 Tailwind CSS 3 主题切换方案 。核心思路很简单:CSS 变量做容器,Tailwind 做钩子。不仅能轻松切换橙色、蓝色、紫色等多套主题,还能顺带把暗黑模式给搞定,话不多说,直接上干货!
| 紫色 | 橙色 | 蓝色 |
|---|---|---|
![]() |
![]() |
![]() |
二、核心思路:CSS 变量 + Tailwind 配置
2.1 为什么要用 CSS 变量?
很多人用 Tailwind 喜欢直接在 tailwind.config.js 里写死颜色,比如 primary: '#FF7300'。这在小项目没问题,一旦需要动态切换,这种方式就显得很僵硬。
更优雅的做法是把 Tailwind 当作"消费者",把 CSS 变量当作"提供者"。
打个比方:
- CSS 变量 就像是房子的**"混凝土结构"**,我们在底层定义好各种颜色的名字(如
--color-primary)。 - Tailwind 类名就像是**"精装修"**,它不关心水泥砂浆是哪个牌子,只认名字。当你把底层的"橙色水泥"换成"蓝色水泥"时,上面的装修(UI 样式)会自动跟着变。
这套方案的架构逻辑如下:
- Config 层:让 Tailwind 的颜色去读取 CSS 变量。
- CSS 层:定义不同主题下的变量值(橙、蓝、紫、暗黑)。
- JS 层 :控制
html标签上的属性,触发 CSS 变量的切换。
三、Step 1:改造 Tailwind 配置
打开 tailwind.config.js,我们需要告诉 Tailwind:"以后遇到 bg-primary,别去死板地找颜色,去 CSS 变量里找"。
核心代码如下:
js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class', // 🔥 关键点:开启 class 模式的暗色模式
theme: {
extend: {
colors: {
primary: {
DEFAULT: 'var(--color-primary)', // 核心逻辑:引用 CSS 变量
hover: 'var(--color-primary-hover)',
// ... 其他色阶
500: 'var(--color-primary-500)',
// 你甚至可以定义 50-900 的色阶,全部映射到 CSS 变量
},
// 其他功能性颜色 success, warning, danger 同理...
text: {
main: 'var(--color-text-main)',
muted: 'var(--color-text-muted)',
},
bg: {
alt: 'var(--color-bg-alt)',
}
},
},
},
plugins: [],
};
📌 核心结论 : 只要配置了 primary: 'var(--color-primary)',你在代码里写 <button class="bg-primary text-white">,Tailwind 编译时就会乖乖地把它替换成 background-color: var(--color-primary)。这一步是地基,一定要打牢。
四、Step 2:定义 CSS 变量库
接下来,我们需要在 CSS 文件中(比如 variables.css)定义这些变量的具体值。这里利用了 CSS 的层级覆盖特性。
4.1 默认主题(橙色)
默认情况下,我们定义一套橙色主题:
css
:root {
/* 品牌色 - 橙色 */
--color-primary: #FE7300;
--color-primary-hover: #E66800;
/* ... 省略大量色阶代码 ... */
/* 文字色、背景色等基础变量 */
--color-text-main: #0F172A;
--color-bg-alt: #F8FAFC;
}
4.2 多品牌主题(蓝、紫)
如果用户想换成蓝色主题,我们不需要改代码,只需要在 html 标签上加一个 data-theme='blue',然后在 CSS 里针对这个属性做变量覆盖:
css
/* 蓝色主题 */
[data-theme='blue'] {
--color-primary: #3B82F6; /* 覆盖主色 */
--color-primary-hover: #2563EB;
/* 覆盖对应色阶... */
}
/* 紫色主题 */
[data-theme='purple'] {
--color-primary: #8B5CF6;
--color-primary-hover: #7C3AED;
/* ... */
}
4.3 暗色模式
暗色模式不需要额外的 data-theme,因为 Tailwind 开启了 darkMode: 'class',我们只需要针对 .dark 类重置变量即可:
css
/* 暗色模式 */
:root.dark {
/* 暗色模式下,文字变白,背景变黑 */
--color-text-main: #F8FAFC;
--color-text-muted: #94A3B8;
--color-bg-alt: #0F172A; /* 深蓝黑背景 */
--color-border: #334155;
}
🌟 注意事项 : 定义 CSS 变量时,命名一定要规范 !比如 --color-primary-50 到 --color-primary-900,最好和 Tailwind 的默认色阶命名保持一致,这样后期维护心智负担最小。
五、Step 3:JS 逻辑控制(核心交互)
有了配置和样式,还需要一段 JS 代码来负责"搬运"------也就是给 HTML 标签增删属性。我们把这些逻辑封装到 theme.js 里。
核心逻辑拆解:
javascript
// 1. 定义支持的主题
export const themes = {
orange: 'orange',
blue: 'blue',
purple: 'purple'
};
// 2. 设置主题函数
export function setTheme(theme) {
if (!themes[theme]) return;
// 🔥 核心操作:切换 data-theme 属性
if (theme === 'orange') {
document.documentElement.removeAttribute('data-theme'); // 默认主题移除属性
} else {
document.documentElement.setAttribute('data-theme', theme);
}
// 💾 存入本地存储,刷新不丢失
localStorage.setItem('mohub-theme', theme);
}
// 3. 切换暗色模式
export function setDarkMode(isDark) {
const html = document.documentElement;
if (isDark) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
localStorage.setItem('mohub-dark-mode', isDark ? 'true' : 'false');
}
// 4. 初始化:页面加载时读取缓存
export function initTheme() {
const savedTheme = localStorage.getItem('mohub-theme');
const savedDarkMode = localStorage.getItem('mohub-dark-mode');
if (savedTheme && themes[savedTheme]) {
setTheme(savedTheme);
}
if (savedDarkMode === 'true') {
setDarkMode(true);
}
return savedTheme;
}
总结一下 : 这段代码就像是开关控制员 。当你调用 setTheme('blue') 时,它就去改 HTML 属性;CSS 监测到属性变了,就会自动应用新的变量值;UI 也就跟着变了。一气呵成,丝般顺滑。
六、Step 4:在 Vue 组件中实战
最后,我们在组件里怎么用?非常简单,就像平时写 Tailwind 一样,不需要任何心理负担。
html
<template>
<div>
<!-- 切换器 -->
<el-select v-model="currentTheme" @change="changeTheme">
<el-option label="橙色" value="orange" />
<el-option label="蓝色" value="blue" />
<el-option label="紫色" value="purple" />
</el-select>
<!-- 这里的 bg-primary 会自动随主题变色 -->
<button class="bg-primary hover:bg-primary-hover text-white px-4 py-2">
主要按钮
</button>
<!-- 背景色和文字色同理 -->
<div class="bg-primary-light p-4">浅色背景容器</div>
<p class="text-primary">主题色文字</p>
</div>
</template>
<script>
import { setTheme, initTheme, getThemeList } from '@/utils/theme';
export default {
data() {
return {
currentTheme: 'orange',
themeList: getThemeList()
};
},
created() {
// 初始化读取上一次的选择
this.currentTheme = initTheme();
},
methods: {
changeTheme() {
setTheme(this.currentTheme);
}
}
};
</script>
亲测有效,你在下拉框切个蓝色,按钮瞬间变蓝,毫秒级响应,完全没有那种传统 Sass 变量替换需要重新编译的延迟感。
七、避坑指南(这 3 个坑我替你踩过了)
虽然方案很完美,但在实际落地时,有几个高频坑点一定要注意:
7.1 服务端渲染(SSR)闪烁问题
如果是 Next.js 或 Nuxt.js 项目,页面初始化时 JS 可能还没执行完,此时 localStorage 没读取到,用户会先看到一瞬间的默认色(比如橙色),然后才闪变成用户保存的蓝色。 解决方案 :在 index.html 的 <head> 标签里加一段内联脚本,在页面渲染前就先把 class 或 data-theme 给加上,虽然写起来有点丑,但能治好闪烁。
7.2 暗色模式下的颜色映射
别以为切了 dark 类就万事大吉了。在暗色模式下,如果你用了 bg-primary-100 这种浅色背景,一定要检查它在 CSS 变量里对应的值是否适合暗黑背景。 建议 :暗黑模式下,尽量使用 bg-primary-900 或者专门定义 bg-primary-dark 这种变量,别直接套用浅色阶,不然对比度不够,看字费眼。
7.3 第三方组件库兼容性
如果你用了 Element Plus 或 Ant Design,它们通常也有自己的暗色模式。 注意 :Tailwind 的 dark 类加在 html 上,往往会自动触发这些组件库的暗色样式(如果它们支持 CSS 变量),但最好还是确认一下。如果冲突了,可能需要把 Tailwind 的 dark 类加在更外层的容器上,而不是 html 上。
八、总结
这套方案的核心优势在于解耦:
- 样式与配置解耦:不用改 Tailwind 配置就能换色。
- 逻辑与样式解耦:JS 只负责改类名,CSS 负责变颜色,各司其职。
对于需要做多租户 SaaS 平台、或者对 UI 细节要求较高的 C 端产品,这套方案是目前的最优解之一。它既保留了 Tailwind 原子化开发的爽快感,又弥补了它在动态主题上的短板。
技术的本质是解决问题,选择合适的工具,才能让自己从重复劳动中解放出来。别再手动去改每一行颜色代码了,试试这套方案,把时间花在更有价值的业务逻辑上吧!
拓展阅读:
我是海潮,专注前端/全栈技术分享,深耕前端工程化领域 5 年,关注我,一起成长、少踩坑 ✨。


