Element Plus 主题定制完全指南:SCSS 变量与 CSS 变量两种改造路径详解
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
Element Plus 基于 BEM 命名规范的 CSS 架构与可编程的 SCSS 变量系统,为开发者提供了从"微调单个组件"到"整体换肤"的完整定制链路。本文以官方文档 theming.md 为主线,结合 theme-chalk 包的真实源码,系统讲解通过 SCSS 变量(编译期改造)与 CSS 变量(运行期改造)两种路径自定义主题的完整实操方案。读完后你将掌握全量导入与按需引入两种场景下的换肤配置、@use/@forward的正确用法,以及用脚本动态控制主题色的能力。
一、为什么需要主题定制:从 BEM 到变量化改造
Element Plus 的样式(theme-chalk)采用 BEM(Block Element Modifier)命名规范书写,类名结构如el-button__inner--primary,这种规范保证了样式隔离性,让逐条覆写成为可能。但如果你需要大规模替换样式——例如把整套主题色从蓝色换成橙色或绿色——逐个覆盖.el-button:hover、.el-select__dropdown等类就不再可行,维护成本与出错概率都会急剧上升。
为此,Element Plus 提供四种改变样式变量的途径:
- SCSS 变量覆盖:在编译期通过
@forward ... with覆写theme-chalk的 SCSS 变量; - CSS 变量覆盖:在运行期直接覆写以
--el-开头的 CSS 自定义属性; - 组件级内联 CSS 变量:针对单个组件实例做局部定制;
- 脚本动态控制:通过
getComputedStyle/element.style读写 CSS 变量,实现运行时换肤。
下面逐一展开。
二、方式一:通过 SCSS 变量定制(全量导入场景)
2.1theme-chalk的 SCSS 变量是如何组织的
theme-chalk全部使用 SCSS 编写,所有可定制变量集中在 packages/theme-chalk/src/common/var.scss 中(该文件共 1700+ 行,覆盖全局配色、文字、边框、填充、背景、间距、字体以及每个组件的专属变量)。
从源码结构看,变量以Sass Map(映射表)形式组织,而不是零散的顶层标量。例如颜色统一存放在$colors这张 map 里,组件变量则各自独立成表,如$notification存放 notification 组件的全部样式变量。这样做的好处是:通过一次map.deep-merge就能整体注入新值,无需逐个变量覆写。
var.scss中$colors的默认定义如下(见 var.scss):
// types $types: primary, success, warning, danger, error, info; // Color $colors: () !default; $colors: map.deep-merge( ( 'white': #ffffff, 'black': #000000, 'primary': ( 'base': #409eff, ), 'success': ( 'base': #67c23a, ), 'warning': ( 'base': #e6a23c, ), 'danger': ( 'base': #f56c6c, ), 'error': ( 'base': #f56c6c, ), 'info': ( 'base': #909399, ), ), $colors );几个关键点:
!default+map.deep-merge组合:$colors先声明为空 map(() !default),再与内置默认色板深合并。这意味着外部通过@forward ... with传入的$colors会合并而非整体替换——你只覆写primary一项,其余颜色保持不变,这正是"按需覆盖"的基础。- 每个色系只需定义
base:如'primary': ('base': #409eff)。后续浅色梯度(light-1~light-9)和深色梯度(dark-2)由源码中的set-color-mix-levelmixin 自动生成(见 var.scss):它调用sass:color的color.mix(),把base色与白色按 10% ~ 90% 比例混合生成 9 档浅色,与黑色按 20% 混合生成 1 档深色:
// --el-color-primary-light-i // 10% 53a8ff 20% 66b1ff ... 90% ecf5ff @each $type in $types { @for $i from 1 through 9 { @include set-color-mix-level($type, $i, 'light', $color-white); } } // --el-color-primary-dark-2 @each $type in $types { @include set-color-mix-level($type, 2, 'dark', $color-black); }这也解释了文档中的提醒:引入element/index.scss必须早于 element-plus 的 scss——只有先注入你的自定义base色,这套自动生成机制才能基于你的主题色产出配套的light-x/dark-2梯度,否则会出现"混合变量"问题(混入了默认色的梯度值)。
2.2 关于@use与@import的取舍
theme-chalk已全面采用Sass Modules(sass:map等内置模块与@use规则)重构了全部 SCSS 变量。使用@use而非@import的关键收益是解决@import导致的重复输出问题——@import每次引入都会重复展开样式与变量,而@use对同一模块只加载一次,输出体积更可控。Sass 官方也已宣布将逐步移除@import,因此你的覆写代码同样应遵循@use语法。
2.3 覆写步骤:全量导入场景
如果你的项目本身使用 SCSS,可以直接修改 Element Plus 的样式变量。共分三步:
第一步:创建覆写文件styles/element/index.scss,只覆写需要的部分:
/* just override what you need */ @forward 'element-plus/theme-chalk/src/common/var.scss' with ( $colors: ( 'primary': ( 'base': green, ), ) ); // If you just import on demand, you can ignore the following content. // if you want to import all styles: // @use "element-plus/theme-chalk/src/index.scss" as *;说明:
@forward ... with (...)会把括号中的新值传给var.scss参与其$colors的 deep-merge,是覆写库变量唯一合规的入口;- 若全量引入样式,可解除
@use "element-plus/theme-chalk/src/index.scss" as *;的注释;按需引入(unplugin)场景则忽略该行,见下文第三、四节; theme-chalk全量样式的真实入口在 packages/theme-chalk/src/index.scss,它通过@use './base.scss'(引入 base.scss 的全局变量、过渡与图标样式)再逐行@use引入 110+ 个组件样式文件。
第二步:在项目入口文件(如main.ts)导入该覆写文件,替代 Element Plus 内置 CSS:
import { createApp } from 'vue' import './styles/element/index.scss' import ElementPlus from 'element-plus' import App from './App.vue' const app = createApp(App) app.use(ElementPlus)这里有两个官方强调的实践细节:
- 导入顺序:
element/index.scss必须在 Element Plus 相关 scss 之前导入,以保证light-x梯度基于你的自定义变量生成; - 文件隔离:建议把
element/index.scss单独作为一个"合并层"文件,将你的业务 scss 与 element 变量 scss 区分开。若混在一起,每次 element-plus 的热更新都需要重新编译大量 scss 文件,导致开发期编译显著变慢;隔离后 element 变量文件独立缓存,热更新只重编译你的业务样式。
三、方式二:按需引入场景的 SCSS 定制(Vite)
按需引入时(如搭配unplugin-vue-components或unplugin-element-plus),组件样式是被单独按需拉取的,无法通过一次性全量入口注入变量。此时需要借助 Vite 的scss.additionalData,让每个被编译的组件 scss 都自动携带你的覆写变量:
import path from 'path' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // You can also use unplugin-vue-components // import Components from 'unplugin-vue-components/vite' // import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' // or use unplugin-element-plus import ElementPlus from 'unplugin-element-plus/vite' export default defineConfig({ resolve: { alias: { '~/': `${path.resolve(__dirname, 'src')}/`, }, }, css: { preprocessorOptions: { scss: { additionalData: `@use "~/styles/element/index.scss" as *;`, }, }, }, plugins: [ vue(), // use unplugin-vue-components // Components({ // resolvers: [ // ElementPlusResolver({ // importStyle: "sass", // // directives: true, // // version: "2.1.5", // }), // ], // }), // or use unplugin-element-plus ElementPlus({ useSource: true, }), ], })要点解读:
additionalData注入:Vite 会把@use "~/styles/element/index.scss" as *;前置拼接到每个 scss 文件,使每个按需加载的组件样式都先经过你的变量覆写层;useSource: true:让unplugin-element-plus引用theme-chalk的SCSS 源码(而非编译后的 CSS),覆写才生效;- 路径别名
~/:指向src目录,additionalData中的~/styles/element/index.scss即上文的覆写文件; - 若改用
unplugin-vue-components的ElementPlusResolver,则通过importStyle: 'sass'开启 sass 源码模式(配置见上注释),二选一即可。
四、方式三:按需引入场景的 SCSS 定制(Webpack)
Webpack 项目使用unplugin-element-plus/webpack插件,并在css.loaderOptions.scss.additionalData中做同样的变量注入:
// use unplugin-element-plus import ElementPlus from 'unplugin-element-plus/webpack' export default defineConfig({ css: { loaderOptions: { scss: { additionalData: `@use "~/styles/element/index.scss" as *;`, }, }, }, plugins: [ ElementPlus({ useSource: true, }), ], })与 Vite 版本唯一的差异是注入位置由preprocessorOptions.scss变为loaderOptions.scss,其余逻辑(useSource: true、覆写文件路径)完全一致。
五、方式四:通过 CSS 变量定制(运行期换肤)
5.1 CSS 变量体系是如何从 SCSS 生成的
CSS 自定义属性(Custom Properties)已被几乎所有现代浏览器支持(IE 除外)。Element Plus 已用 CSS 变量重构了几乎所有组件的样式体系,并且这套体系与 SCSS 变量系统天然兼容——源码通过 SCSS 函数在编译期自动把 map 中的值转换成 CSS 变量输出,你无需手动维护两套变量。
从源码看,这套转换由 packages/theme-chalk/src/mixins/_var.scss 和 packages/theme-chalk/src/mixins/function.scss 协作完成:
- function.scss 的
joinVarName生成变量名:把('color', 'primary')拼成--el-color-primary(--+ 命名空间el+ 逐项-连接); _var.scss的set-css-var-value/set-css-var-type负责写值,例如set-css-var-type('color', 'primary', $colors)输出--el-color-primary: #409eff;- 每个组件样式入口都会先调用
set-component-css-var为组件声明专属变量,例如 button.scss 中的set-component-css-var('button', $button)生成--el-button-*系列,tag.scss 生成--el-tag-*系列。
因此你看到的所有--el-*变量,实际都是源码中 SCSS map 的编译产物——不修改 scss、不重新编译,也能在运行期精准改写任意变量,这就是 CSS 变量路径的核心优势。
5.2 全局覆写主题色
最简单的方式是在:root上覆写变量:
:root { --el-color-primary: green; }注意,只需覆写--el-color-primary(即base色),组件的 hover、disabled、渐变等衍生态会通过var(--el-color-primary, ...)引用链自动跟随变化——这正是 CSS 变量相比硬编码值的关键价值。
5.3 单组件级定制
只想定制某个组件时,不必触碰全局,直接在该组件元素上写内联 CSS 变量即可,例如把 Tag 组件的背景色改为红色:
<el-tag style="--el-tag-bg-color: red">Tag</el-tag>--el-tag-bg-color正是 tag.scss 中由css-var-from-global(('tag', 'bg-color'), ...)生成并挂载到.el-tag类上的组件变量。
5.4 类作用域定制(推荐)
出于性能考虑,官方更推荐把 CSS 变量收敛在某个类下,而非全局:root——这样变量只对该类覆盖范围内的 DOM 生效,避免全页面样式级联重算:
.custom-class { --el-tag-bg-color: red; }5.5 用脚本动态控制主题(运行时换肤)
CSS 变量可以在运行期用原生 DOM API 读写,从而实现真正的动态换肤:
// document.documentElement is global const el = document.documentElement // const el = document.getElementById('xxx') // get css var getComputedStyle(el).getPropertyValue(`--el-color-primary`) // set css var el.style['--el-color-primary'] = 'red'读取用getComputedStyle(el).getPropertyValue('--el-color-primary'),写入直接操作el.style的对应属性。若想更优雅地声明式管理变量,可借助 VueUse 的useCssVar组合式函数将其封装为响应式状态,在组件中直接读写。
六、两条路径如何选择
| 维度 | SCSS 变量(编译期) | CSS 变量(运行期) |
|---|---|---|
| 生效时机 | 构建时,需重新编译 | 运行期,即时生效 |
| 改动范围 | 一次覆写,全局统一 | 可按全局/类/单元素分级控制 |
| 衍生色(light-x/dark-2) | 由color.mix自动重算 | 引用链自动跟随base变化 |
| 按需引入 | 需additionalData+useSource: true | 天然支持,无需构建配置 |
| 动态换肤 | 不支持 | 支持(脚本读写变量) |
| 适用场景 | 品牌色固化、多环境统一主题 | 暗色模式、用户偏好切换、局部微调 |
两者并非互斥:实践中最常见的组合是编译期用 SCSS 变量固化品牌基调,运行期用 CSS 变量做局部覆写与动态切换。若需查阅每个组件当前可定制的全部变量名,可直接阅读 packages/theme-chalk/src/common/var.scss(全局与各组件 map 均在其中),或查看各组件样式入口(如 tag.scss、button.scss)中set-component-css-var的调用位置。
七、常见问题与避坑清单
- 混合变量报错:覆写文件必须在使用 element scss 之前导入,否则
light-x梯度按默认色生成,与你注入的base不一致; @import废弃警告:所有覆写与业务 scss 一律使用@use/@forward,避免重复输出与未来兼容风险;- 按需引入不生效:确认插件开启
useSource: true(Vite 与 Webpack 均需),且additionalData路径别名(~/)解析正确; - 热更新变慢:不要把业务 scss 与 element 变量 scss 混在一个文件里,保持
element/index.scss独立成层; - 只覆写需要的变量:
map.deep-merge保证只合并你传入的键,无需(也不建议)复制整份$colors定义。
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考