Element Plus 主题定制与暗色模式

1. 三种定制层级,按需选用

大多数项目并不需要"深度定制 Element Plus 主题",往往只是改一下主色。但不同的需求对应不同的改法,混着用会互相覆盖翻车:

需求 方法 生效时机 复杂度
只改主色(如蓝 → 绿) 覆盖 CSS 变量 运行时
全面改品牌色(10+ 个变量) SCSS 变量编译覆盖 构建时 ★★
白天/黑夜切换 官方暗色主题 + 切换 .dark 运行时 ★★
用户自选多套主题 运行时动态改 CSS 变量 运行时 ★★★

一条原则能用 CSS 变量解决的,绝不上 SCSS。因为 CSS 变量能运行时切换,SCSS 改一次要重新构建。

Java 对照


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 *));

这样:

一箭双雕:一个类控制两套 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>

问题:

  1. !important 是 CSS 优先级核弹,后续想再调整会更难
  2. Element Plus 升级版本时,内部 DOM 结构可能变,你的硬覆盖会错位
  3. 团队其他人看到这种代码会跟着写,形成恶性循环

正确思路(按优先级排序):

  1. 组件能解决:用 typesize 等 props,而不是 class
  2. CSS 变量能解决:覆盖对应的 --el-xxx 变量
  3. 实在不行:外层包一个自定义类,在 :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. 排查流程:样式不生效时

改主题时样式总是不对?按这个顺序查:

  1. DevTools → Elements → 选中元素 → Styles 面板
    • 看实际应用的是哪条规则,哪里被划掉(被覆盖)
    • 看 Computed 里 --el-color-primary 的最终值
  2. 检查 style.css / SCSS 的导入顺序
    • 覆盖一定要在原样式后面导入
  3. 检查作用域
    • <style scoped> 不会穿透到 Element Plus 组件,要用 :deep()
  4. 检查变量名拼写
    • --el-color-primary-light-3 不是 --el-primary-light-3
  5. SCSS 没生效?
    • 确认 ElementPlusResolver({ importStyle: 'sass' }) 加了
    • node_modules/.vite 缓存后重启 dev server

小练习

  1. 把 in4vue 主色改成 #8b5cf6(紫色),验证所有 type="primary" 的组件都变色
  2. 接入 useDark,在顶栏加切换按钮,刷新页面依然记得状态
  3. <el-color-picker> + watchEffect 做一个运行时换色功能
  4. 把分页器改成中文(el-config-provider + zhCn
  5. 打开 DevTools 的 Computed 面板,看 .dark 模式下 --el-bg-color 变成了什么颜色

延伸阅读