Element Plus 主题定制与暗色模式
1. 三种定制层级,按需选用
大多数项目并不需要"深度定制 Element Plus 主题",往往只是改一下主色。但不同的需求对应不同的改法,混着用会互相覆盖翻车:
| 需求 | 方法 | 生效时机 | 复杂度 |
|---|---|---|---|
| 只改主色(如蓝 → 绿) | 覆盖 CSS 变量 | 运行时 | ★ |
| 全面改品牌色(10+ 个变量) | SCSS 变量编译覆盖 | 构建时 | ★★ |
| 白天/黑夜切换 | 官方暗色主题 + 切换 .dark |
运行时 | ★★ |
| 用户自选多套主题 | 运行时动态改 CSS 变量 | 运行时 | ★★★ |
一条原则:能用 CSS 变量解决的,绝不上 SCSS。因为 CSS 变量能运行时切换,SCSS 改一次要重新构建。
Java 对照:
- CSS 变量覆盖 ≈
application.yml里改配置,重启生效、运行时也可改 - SCSS 变量 ≈ Maven
pom.xml里的<properties>,编译期绑死 - 官方暗色主题 ≈ Spring 的
@Profile("dark"),切换激活的配置集
2. 最简改法:覆盖 CSS 变量
Element Plus 2.x 几乎所有样式都走 CSS 变量。打开 DevTools 看任何 <el-button type="primary">,你会看到:
.el-button--primary {
--el-button-bg-color: var(--el-color-primary);
--el-button-border-color: var(--el-color-primary);
/* ... */
}
核心变量是 --el-color-primary。覆盖它就够了。
2.1 改主色(全站生效)
在 src/style.css 里加几行:
@import 'tailwindcss';
/* 覆盖 Element Plus 主题变量 */
:root {
--el-color-primary: #10b981; /* 主色:绿色 */
--el-color-primary-light-3: #34d399;
--el-color-primary-light-5: #6ee7b7;
--el-color-primary-light-7: #a7f3d0;
--el-color-primary-light-8: #d1fae5;
--el-color-primary-light-9: #ecfdf5;
--el-color-primary-dark-2: #059669;
}
为什么要写 light/dark 变体:Element Plus 的 hover、disabled、边框、背景等状态用的是这些派生色,只改 --el-color-primary 会导致 hover 效果错乱。
偷懒做法:去 Element Plus 官方配色工具 输入主色,自动生成全套变体。
2.2 验证
<el-button type="primary">按钮</el-button>
<el-tag type="primary">标签</el-tag>
按钮和标签变绿,改完。
3. SCSS 变量覆盖(深度定制才用)
只有在你想改特定组件的默认样式(比如所有 el-card 的圆角、所有 el-input 的高度)时才用 SCSS。
3.1 安装 sass
in4vue 的 package.json 已经装了 "sass": "^1.99.0",直接用。
3.2 建一个 SCSS 覆盖文件
/* src/styles/element-overrides.scss */
/* 先覆盖 Element Plus 的 SCSS 变量 */
@use 'element-plus/theme-chalk/src/common/var.scss' as * with (
$colors: (
'primary': (
'base': #10b981,
),
),
$border-radius: (
'base': 8px, // 所有组件的基础圆角
'small': 4px,
),
);
/* 再引入 Element Plus 所有样式,此时已经应用了上面的覆盖 */
@use 'element-plus/theme-chalk/src/index.scss' as *;
3.3 让按需导入走这个文件
Element Plus 按需导入时默认引原生 CSS,要切到你的 SCSS,需要改 Vite 配置:
// vite.config.ts
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
Components({
resolvers: [
ElementPlusResolver({
importStyle: 'sass', // 关键:切到 sass
}),
],
}),
然后 main.ts:
import './styles/element-overrides.scss'
注意:切到 SCSS 后构建会变慢(每次都要编译 Element Plus 的所有 SCSS),所以没有深度定制需求就别碰。
Java 对照:类似 Spring 的 @ConditionalOnProperty + 自定义配置类——你替换了默认 Bean,整个上下文都跟着变。代价是构建/启动开销变大。
4. 暗色模式:官方方案
Element Plus 2.2+ 内置暗色主题,不用自己改颜色。
4.1 引入暗色样式
src/main.ts:
import 'element-plus/theme-chalk/dark/css-vars.css'
4.2 切换 <html> 的类
// 切换暗色模式
document.documentElement.classList.toggle('dark')
加了 .dark 类,Element Plus 的 --el-color-xxx、--el-bg-color 等变量自动切换到暗色值。
4.3 和 Tailwind 的联动
Tailwind v4 的暗色模式前缀 dark: 默认通过 prefers-color-scheme 触发,但我们要手动切换(因为要和按钮点击联动),所以在 style.css 里声明变体:
@import 'tailwindcss';
@import 'element-plus/theme-chalk/dark/css-vars.css';
@custom-variant dark (&:where(.dark, .dark *));
这样:
dark:bg-gray-900这种 Tailwind 类在.dark下生效- Element Plus 的组件(
el-button、el-table等)也自动切到暗色
一箭双雕:一个类控制两套 UI 的主题。
4.4 持久化 + 跟随系统
用 VueUse 的 useDark,一步到位:
// src/composables/useTheme.ts
import { useDark, useToggle } from '@vueuse/core'
// useDark 自动:
// 1. 读取 localStorage 里的偏好
// 2. 没偏好时跟随系统
// 3. 切换时写 localStorage + 加/移除 html.dark
export const isDark = useDark()
export const toggleDark = useToggle(isDark)
在顶栏加一个切换按钮:
<script setup lang="ts">
import { isDark, toggleDark } from '@/composables/useTheme'
import { Moon, Sunny } from '@element-plus/icons-vue'
</script>
<template>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700" @click="toggleDark()">
<el-icon><Moon v-if="!isDark" /><Sunny v-else /></el-icon>
</button>
</template>
刷新页面、重启浏览器都记得之前的选择——useStorage 做了同样的事,useDark 是它的"暗色专用封装"。
5. 运行时动态改主色(进阶)
让用户自己选颜色(QQ 皮肤风格)怎么做?用 CSS 变量 + JS 动态改:
// src/composables/useThemeColor.ts
import { computed, watchEffect } from 'vue'
import { useStorage } from '@vueuse/core'
export const primaryColor = useStorage('theme-primary', '#10b981')
// 主色的派生色(简化版:手动混白)
function mix(color: string, weight: number): string {
// color: #rrggbb, weight: 0-1 之间,0 = 原色,1 = 纯白
const r = parseInt(color.slice(1, 3), 16)
const g = parseInt(color.slice(3, 5), 16)
const b = parseInt(color.slice(5, 7), 16)
const nr = Math.round(r + (255 - r) * weight)
const ng = Math.round(g + (255 - g) * weight)
const nb = Math.round(b + (255 - b) * weight)
return `#${[nr, ng, nb].map(v => v.toString(16).padStart(2, '0')).join('')}`
}
watchEffect(() => {
const root = document.documentElement
root.style.setProperty('--el-color-primary', primaryColor.value)
for (let i = 1; i <= 9; i++) {
root.style.setProperty(`--el-color-primary-light-${i}`, mix(primaryColor.value, i * 0.1))
}
})
配合 Element Plus 的 <el-color-picker>:
<el-color-picker v-model="primaryColor" />
用户选色后整站主色实时变化。
进阶技巧:混色不要自己写,用 Element Plus 内部的 tinycolor2 或 @ctrl/tinycolor,算法更准确。
6. 反模式:不要用 !important 硬覆盖
看到网上教程说"组件样式改不动?加 !important",劝你别学。
<!-- ❌ 反模式 -->
<el-button class="!bg-red-500 !rounded-none">...</el-button>
问题:
!important是 CSS 优先级核弹,后续想再调整会更难- Element Plus 升级版本时,内部 DOM 结构可能变,你的硬覆盖会错位
- 团队其他人看到这种代码会跟着写,形成恶性循环
正确思路(按优先级排序):
- 组件能解决:用
type、size等 props,而不是 class - CSS 变量能解决:覆盖对应的
--el-xxx变量 - 实在不行:外层包一个自定义类,在
:deep()里改
<template>
<div class="my-wrapper">
<el-button>提交</el-button>
</div>
</template>
<style scoped>
.my-wrapper :deep(.el-button) {
border-radius: 12px;
}
</style>
:deep() 是 Vue scoped 样式的"穿透选择器",正式进入子组件内部。作用域可控、优先级合理、升级不易翻车。
7. 全局 size 和 locale 配置
主题定制之外,还有两件"一次配置全站生效"的事:
7.1 全局默认尺寸
<!-- src/App.vue -->
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
</script>
<template>
<el-config-provider :size="'default'" :z-index="3000">
<router-view />
</el-config-provider>
</template>
size 支持 large / default / small,影响所有组件的默认尺寸。
7.2 中文国际化
Element Plus 默认英文,分页器显示 "Total 100 items",不换掉会被产品吐槽:
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<template>
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
分页器立刻变成"共 100 条"。
Java 对照:类似 Spring 的 LocaleResolver + messages_zh_CN.properties,一次配置,所有 UI 文案走这套词典。
8. in4vue 推荐的完整配置
把上面的好习惯合并起来,src/main.ts 大致是这样:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 样式顺序很重要:
// 1. Tailwind preflight 先(重置浏览器默认)
import './style.css'
// 2. Element Plus 暗色主题变量
import 'element-plus/theme-chalk/dark/css-vars.css'
// 3. 自己的覆盖最后(优先级最高)
import './styles/overrides.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
src/styles/overrides.css:
/* Element Plus 主色 */
:root {
--el-color-primary: #10b981;
--el-color-primary-light-3: #34d399;
--el-color-primary-light-5: #6ee7b7;
--el-color-primary-light-7: #a7f3d0;
--el-color-primary-light-8: #d1fae5;
--el-color-primary-light-9: #ecfdf5;
--el-color-primary-dark-2: #059669;
}
/* 暗色下主色可以更柔和一点 */
.dark {
--el-color-primary: #34d399;
}
src/App.vue:
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<template>
<el-config-provider :locale="zhCn" size="default">
<router-view />
</el-config-provider>
</template>
9. 排查流程:样式不生效时
改主题时样式总是不对?按这个顺序查:
- DevTools → Elements → 选中元素 → Styles 面板
- 看实际应用的是哪条规则,哪里被划掉(被覆盖)
- 看 Computed 里
--el-color-primary的最终值
- 检查
style.css/ SCSS 的导入顺序- 覆盖一定要在原样式后面导入
- 检查作用域
<style scoped>不会穿透到 Element Plus 组件,要用:deep()
- 检查变量名拼写
--el-color-primary-light-3不是--el-primary-light-3
- SCSS 没生效?
- 确认
ElementPlusResolver({ importStyle: 'sass' })加了 - 清
node_modules/.vite缓存后重启 dev server
- 确认
小练习
- 把 in4vue 主色改成
#8b5cf6(紫色),验证所有type="primary"的组件都变色 - 接入
useDark,在顶栏加切换按钮,刷新页面依然记得状态 - 用
<el-color-picker>+watchEffect做一个运行时换色功能 - 把分页器改成中文(
el-config-provider+zhCn) - 打开 DevTools 的 Computed 面板,看
.dark模式下--el-bg-color变成了什么颜色
延伸阅读
- Element Plus - 主题
- Element Plus - 暗黑模式
- Element Plus - 国际化
- VueUse - useDark
- CSS 自定义属性(变量)(MDN,搞清楚变量继承和作用域)