前言
本篇文章主要讲解如何实现 Element Plus 主题色的动态切换
说点关于专栏的话
在不久前,我习惯性在掘金翻阅文章和我的主页,注意起专栏的阅读数据一直在极缓慢的升高,专栏订阅数也有 40+ 人(虽然不算多,但至少还能证明有读者愿意读)
我想,可能这些文章对于读者还是有一点点帮助的
本文也是《通俗易懂的中后台系统建设指南》系列的第七篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统
了解 Element Plus 主题自定义
打开 Element Plus 官网,在进阶部分有一篇自定义主题的文章,文章介绍了几种定制主题色彩的方案,大致分为:
- 通过 SCSS 变量,新建一个样式文件来覆盖默认样式
- 通过 CSS 变量,修改以
--el- 开头的 CSS 变量,通过 CSS 变量覆盖或通过 JS 修改
第一种 SCSS 变量方案,适用于相对固定场景下的色彩定制需求,有 Scss 预编译的加持,定制起来也方便
第二种方案很好,通过 CSS 变量就可以覆盖默认样式,但还不够好,比如你想要修改 Element Plus 默认的主题色调 #409eff,Css 变量名是 --el-color-primary,但是这只修改了一个变量值,并不会生成相对应的色阶值
所以,仅仅修改主题色还不够,至少现在还不够,这里推荐你看看 ELement Plus 讨论区的一篇内容:关于运行时多主题切换的探讨与实现
Element Plus 内部 SCSS 函数会计算出一套主题色阶,供系统全局使用
--el-color-primary
--el-color-primary-light-3
--el-color-primary-light-5
--el-color-primary-light-7
--el-color-primary-light-8
--el-color-primary-light-9
--el-color-primary-dark-2
我们需要全部覆盖,才能完全实现主题色的切换
动态切换 Element Plus 主题实现
上面简单介绍了一下 Element Plus 的自定义主题,这节来聊一下具体实现方案
在目前很多开源的后台系统中,实现动态主题,大致的方案就是借助 JS 来写一套颜色算法函数(参考 Element Plus Scss 色值逻辑),从而取得各色阶的颜色,再用 JS 控制在根元素上覆盖原有的 Css 变量值来达到动态的效果
Element Plus 自定义主题文章在底部,也给出了 JS 控制 Css 变量的示例
// document.documentElement 是全局变量时
const el = document.documentElement
// const el = document.getElementById('xxx')
// 获取 css 变量
getComputedStyle(el).getPropertyValue(`--el-color-primary`)
// 设置 css 变量
el.style.setProperty('--el-color-primary', 'red')
本文也将基于上述方案通过 JS 控制来实现
前期准备
Clean Admin 在根目录下新建了一个 theme 文件夹,表示主题相关的内容
颜色算法函数
上面提到,Element Plus 的颜色算法是通过 SCSS 函数计算得来的,我们现在要做的,就是用 JS 来重写这套函数的色值计算逻辑
也别担心,这里的颜色算法函数不需要你去手写,在各大知名开源后台中的 utils 文件夹下一般都可以找到,实现逻辑大差不差
Clean Admin 的色阶计算函数取自于 Art Design Pro,因为这是最近我觉得比较美观的后台系统,色彩设计不错
在 theme 文件夹下新建一个 helpers.ts 文件,写入以下内容:
如果你想看 Clean Admin 关于这里的源码:点击查看 - helpers.ts
/**
* 颜色转换结果接口
*/
interface RgbaResult {
red: number;
green: number;
blue: number;
rgba: string;
}
/**
* 验证hex颜色格式
* @param hex hex颜色值
* @returns 是否为有效的hex颜色
*/
function isHexColor(hex: string): boolean {
const cleanHex = hex.trim().replace(/^#/, '');
return /^[0-9A-F]{3}$|^[0-9A-F]{6}$/i.test(cleanHex);
}
/**
* 验证RGB颜色值
* @param r 红色值
* @param g 绿色值
* @param b 蓝色值
* @returns 是否为有效的RGB值
*/
function isRgbValue(r: number, g: number, b: number): boolean {
const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255;
return isValid(r) && isValid(g) && isValid(b);
}
/**
* 将hex颜色转换为RGBA
* @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式)
* @param opacity 透明度 (0-1)
* @returns 包含RGB值和RGBA字符串的对象
*/
export function hexToRgba(hex: string, opacity: number): RgbaResult {
if (!isHexColor(hex)) {
throw new Error('Invalid hex color format');
}
// 移除可能存在的 # 前缀并转换为大写
let cleanHex = hex.trim().replace(/^#/, '').toUpperCase();
// 如果是缩写形式(如 FFF),转换为完整形式
if (cleanHex.length === 3) {
cleanHex = cleanHex
.split('')
.map((char) => char.repeat(2))
.join('');
}
// 解析 RGB 值
const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16));
// 确保 opacity 在有效范围内
const validOpacity = Math.max(0, Math.min(1, opacity));
// 构建 RGBA 字符串
const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})`;
return { red, green, blue, rgba };
}
/**
* 将hex颜色转换为RGB数组
* @param hexColor hex颜色值
* @returns RGB数组 [r, g, b]
*/
export function hexToRgb(hexColor: string): number[] {
if (!isHexColor(hexColor)) {
throw new Error('Invalid hex color format');
}
const cleanHex = hexColor.replace(/^#/, '');
let hex = cleanHex;
// 处理缩写形式
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char.repeat(2))
.join('');
}
const hexPairs = hex.match(/../g);
if (!hexPairs) {
throw new Error('Invalid hex color format');
}
return hexPairs.map((hexPair) => parseInt(hexPair, 16));
}
/**
* 将RGB颜色转换为hex
* @param r 红色值 (0-255)
* @param g 绿色值 (0-255)
* @param b 蓝色值 (0-255)
* @returns hex颜色值
*/
export function rgbToHex(r: number, g: number, b: number): string {
if (!isRgbValue(r, g, b)) {
throw new Error('Invalid RGB color values');
}
const toHex = (value: number) => {
const hex = value.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* 颜色混合
* @param color1 第一个颜色
* @param color2 第二个颜色
* @param ratio 混合比例 (0-1)
* @returns 混合后的颜色
*/
export function colourBlend(color1: string, color2: string, ratio: number): string {
const validRatio = Math.max(0, Math.min(1, Number(ratio)));
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
const blendedRgb = rgb1.map((value1, index) => {
const value2 = rgb2[index];
return Math.round(value1 * (1 - validRatio) + value2 * validRatio);
});
return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2]);
}
/**
* 获取变浅的颜色
* @param color 原始颜色
* @param level 变浅程度 (0-1)
* @param isDark 是否为暗色主题
* @returns 变浅后的颜色
*/
export function getLightColor(color: string, level: number, isDark: boolean = false): string {
if (!isHexColor(color)) {
throw new Error('Invalid hex color format');
}
if (isDark) {
return getDarkColor(color, level);
}
const rgb = hexToRgb(color);
const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value));
return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2]);
}
/**
* 获取变深的颜色
* @param color 原始颜色
* @param level 变深程度 (0-1)
* @returns 变深后的颜色
*/
export function getDarkColor(color: string, level: number): string {
if (!isHexColor(color)) {
throw new Error('Invalid hex color format');
}
const rgb = hexToRgb(color);
const darkRgb = rgb.map((value) => Math.floor(value * (1 - level)));
return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2]);
}
/**
* 设置 Css 变量,在根节点上(html)
* @param property 属性名
* @param value 属性值
* @param priority 优先级
*/
export function setHtmlProperty(property: string, value: string | null, priority?: string) {
const style = document.documentElement.style;
style.setProperty(property, value, priority);
}
处理 Element Plus 主题颜色的函数
如下所示,Element Plus 的色阶一般只分 3,5,7,8,9 这五个梯值,只修改这几个值即可完整覆盖主题色
但我认为还不够,这里打算给出更多的色阶变量供选择,比如 1 ~ 9
新建一个 constants.ts 文件,写入一个主题色色阶权重常量,表示我们需要生成 1 ~ 9 的色阶值
点击查看:Clean Admin - constants.ts 代码
/** Element Plus 主题色权重 */
const EL_PRIMARY_COLOR_WEIGHT: ColorWeight = [100, 200, 300, 400, 500, 600, 700, 800, 900];
准备工作做完了,我们可以开始处理 Element Plus 主题颜色了
新建一个 helpers.ts 文件,表示处理 Element Plus 主题色的函数,定义一个 setElementPrimaryColor 函数:
点击查看:Clean Admin - helpers.ts 代码
import { getDarkColor, getLightColor, setHtmlProperty } from './color';
/**
* 处理 Element Plus 主题颜色
* @param color 颜色值
* @param isDark 是否暗黑模式
*/
export function setElementPrimaryColor(color: string, isDark: boolean = false): void {
setHtmlProperty('--el-color-primary', color);
setHtmlProperty(`--el-color-primary-dark-2`, getDarkColor(color, 0.2));
for (const weight of EL_PRIMARY_COLOR_WEIGHT) {
setHtmlProperty(
`--el-color-primary-light-${weight / 100}`,
getLightColor(color, weight / 1000, isDark),
);
}
}
在这个函数中,我们根据传入的主题颜色,首先修改 --el-color-primary 变量和 --el-color-primary-dark-2 变量,这是 Element Plus 关于主题色需要的变量
根据常量 EL_PRIMARY_COLOR_WEIGHT 中定义的梯度色阶,也就是 1 ~ 9 循环计算出对应的色阶值,并使用 setHtmlProperty,将 Css 变量写入根元素 html 下进行覆盖
注意,这里还有个 isDark 参数,用于生成暗黑模式下的色彩值
现在,生成的色阶是这样的:
在 Clean Admin 已经应用了这套色阶算法实现动态切换,现在的效果是这样的:

在 Clean Admin - theme 文件夹找到全部相关文件
还可以做些什么?
至此,关于动态切换主题色的核心操作,基本已经完成,接下来的事可以是
- 预设色彩值、允许用户自定义选择、手动输入色彩值
- 持久化存储主题色
- 适配暗黑模式下的色值变化
持久化存储主题色值
注意,当用户自主选择了色彩偏好时,为了用户体验,建议你保存该值,比如存在本地存储 local 中
这里的持久化存储作用是指当下次打开页面或刷新页面,依然保持用户选择的色彩偏好
Clean Admin 中使用了 Pinia + pinia-plugin-persistedstate 实现,专栏之前文章有讲过
暗黑模式下的色值变化
在 setElementPrimaryColor 函数中,有一个 isDark 参数,当值为 true 时,会生成暗黑模式下的色阶值
如果你的系统有暗黑模式切换的功能,那么在切换暗黑模式时,需要重新调用 setElementPrimaryColor 函数
vueuse 中有一个 useDark 函数,可以响应式获取暗黑模式的状态
一个最简单例子:
import { useDark } from '@vueuse/core'
const isDark = useDark()
watch(isDark, (mode) => {
setElementPrimaryColor('#409eff', mode)
})
拓展太多有点偏题,你可以在 Clean Admin 源码找到相关实现
参考资料
了解更多
系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏
实战项目:vue-clean-admin
交流讨论
文章如有错误或需要改进之处,欢迎指正
前言
本篇文章主要讲解如何实现 Element Plus 主题色的动态切换
说点关于专栏的话
在不久前,我习惯性在掘金翻阅文章和我的主页,注意起专栏的阅读数据一直在极缓慢的升高,专栏订阅数也有 40+ 人(虽然不算多,但至少还能证明有读者愿意读)
我想,可能这些文章对于读者还是有一点点帮助的
了解 Element Plus 主题自定义
打开 Element Plus 官网,在进阶部分有一篇自定义主题的文章,文章介绍了几种定制主题色彩的方案,大致分为:
--el-开头的 CSS 变量,通过 CSS 变量覆盖或通过 JS 修改第一种 SCSS 变量方案,适用于相对固定场景下的色彩定制需求,有 Scss 预编译的加持,定制起来也方便
第二种方案很好,通过 CSS 变量就可以覆盖默认样式,但还不够好,比如你想要修改 Element Plus 默认的主题色调
#409eff,Css 变量名是--el-color-primary,但是这只修改了一个变量值,并不会生成相对应的色阶值所以,仅仅修改主题色还不够,至少现在还不够,这里推荐你看看 ELement Plus 讨论区的一篇内容:关于运行时多主题切换的探讨与实现
Element Plus 内部 SCSS 函数会计算出一套主题色阶,供系统全局使用
--el-color-primary--el-color-primary-light-3--el-color-primary-light-5--el-color-primary-light-7--el-color-primary-light-8--el-color-primary-light-9--el-color-primary-dark-2我们需要全部覆盖,才能完全实现主题色的切换
动态切换 Element Plus 主题实现
上面简单介绍了一下 Element Plus 的自定义主题,这节来聊一下具体实现方案
在目前很多开源的后台系统中,实现动态主题,大致的方案就是借助 JS 来写一套颜色算法函数(参考 Element Plus Scss 色值逻辑),从而取得各色阶的颜色,再用 JS 控制在根元素上覆盖原有的 Css 变量值来达到动态的效果
Element Plus 自定义主题文章在底部,也给出了 JS 控制 Css 变量的示例
本文也将基于上述方案通过 JS 控制来实现
前期准备
Clean Admin 在根目录下新建了一个
theme文件夹,表示主题相关的内容颜色算法函数
上面提到,Element Plus 的颜色算法是通过 SCSS 函数计算得来的,我们现在要做的,就是用 JS 来重写这套函数的色值计算逻辑
也别担心,这里的颜色算法函数不需要你去手写,在各大知名开源后台中的
utils文件夹下一般都可以找到,实现逻辑大差不差Clean Admin 的色阶计算函数取自于 Art Design Pro,因为这是最近我觉得比较美观的后台系统,色彩设计不错
在
theme文件夹下新建一个helpers.ts文件,写入以下内容:处理 Element Plus 主题颜色的函数
如下所示,Element Plus 的色阶一般只分 3,5,7,8,9 这五个梯值,只修改这几个值即可完整覆盖主题色
但我认为还不够,这里打算给出更多的色阶变量供选择,比如 1 ~ 9
新建一个
constants.ts文件,写入一个主题色色阶权重常量,表示我们需要生成 1 ~ 9 的色阶值准备工作做完了,我们可以开始处理 Element Plus 主题颜色了
新建一个
helpers.ts文件,表示处理 Element Plus 主题色的函数,定义一个setElementPrimaryColor函数:在这个函数中,我们根据传入的主题颜色,首先修改
--el-color-primary变量和--el-color-primary-dark-2变量,这是 Element Plus 关于主题色需要的变量根据常量
EL_PRIMARY_COLOR_WEIGHT中定义的梯度色阶,也就是 1 ~ 9 循环计算出对应的色阶值,并使用setHtmlProperty,将 Css 变量写入根元素html下进行覆盖注意,这里还有个
isDark参数,用于生成暗黑模式下的色彩值现在,生成的色阶是这样的:
在 Clean Admin 已经应用了这套色阶算法实现动态切换,现在的效果是这样的:
还可以做些什么?
至此,关于动态切换主题色的核心操作,基本已经完成,接下来的事可以是
持久化存储主题色值
注意,当用户自主选择了色彩偏好时,为了用户体验,建议你保存该值,比如存在本地存储
local中Clean Admin 中使用了
Pinia+pinia-plugin-persistedstate实现,专栏之前文章有讲过暗黑模式下的色值变化
在
setElementPrimaryColor函数中,有一个isDark参数,当值为true时,会生成暗黑模式下的色阶值如果你的系统有暗黑模式切换的功能,那么在切换暗黑模式时,需要重新调用
setElementPrimaryColor函数vueuse中有一个useDark函数,可以响应式获取暗黑模式的状态一个最简单例子:
拓展太多有点偏题,你可以在 Clean Admin 源码找到相关实现
参考资料
了解更多
系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏
实战项目:vue-clean-admin
交流讨论
文章如有错误或需要改进之处,欢迎指正