接手品牌改版时,最费力的往往不是写新页面,而是把一整套组件库的默认蓝一寸寸改掉。Vant ConfigProvider 主题定制正是为解决这类问题而生:它把散落在各组件里的视觉参数收敛成一套 --van-* CSS 变量,让品牌色替换、局部换肤、暗黑模式都能在一处完成。本文基于 Vant 4,从变量体系讲到 theme-vars 的作用域与亮暗双主题令牌,并给出可直接复制的主题切换代码。
先弄清 Vant 的 CSS 变量体系
Vant 4 的样式全部构建在 CSS 自定义属性上,变量分为基础变量与组件变量两类。组件变量默认继承基础变量,例如主色按钮的背景定义在 .van-button--primary 上,而它引用的又是指向调色板的基础变量:
:root {
--van-blue: #1989fa;
--van-white: #fff;
--van-primary-color: var(--van-blue);
--van-button-primary-color: var(--van-white);
--van-button-primary-background: var(--van-primary-color);
}
由此得到一个关键推论:改 --van-primary-color 会连带影响 Button、Slider、Picker、Tag 等一大片组件;改 --van-button-primary-background 只影响主色按钮。前者是设计令牌,后者是局部微调,选错层级是主题定制最常见的翻车点。
| 基础变量 | 默认值 | 影响范围 |
|---|---|---|
--van-primary-color | var(--van-blue) | 主色系组件(按钮、进度条、选择器确认按钮等) |
--van-danger-color | var(--van-red) | 错误态、删除类操作 |
--van-text-color | var(--van-gray-8) | 全局主文本 |
--van-background-2 | var(--van-white) | 弹层、单元格等容器底色 |
--van-radius-lg | 8px | 卡片与弹层圆角 |
--van-padding-md | 16px | 组件通用内边距 |
--van-duration-base | 0.3s | 过渡动画时长 |
四种覆盖方式,别选错作用域
官方文档提供了从纯 CSS 到组件 props 的多条路径,它们的作用域差异比写法差异更重要:
| 方式 | 写法 | 作用域 | 适合场景 |
|---|---|---|---|
| 全局 CSS | :root:root { ... } | 整站 | 品牌色、字体、圆角等基础变量 |
theme-vars(默认) | :theme-vars="vars" | 仅子组件(local) | 单个区块的组件变量微调 |
theme-vars-scope="global" | 同上 + global | 整个页面(挂到 html 根) | 用 JS 动态切换基础变量 |
| 分主题令牌 | theme-vars-dark / theme-vars-light | 对应亮/暗主题 | 两套配色分别定义 |
local 模式把变量写在 ConfigProvider 根节点上,组件变量会优先继承最近的父级基础变量,因此在 local 模式里写 primaryColor 往往不生效。基础变量请用 :root 或 theme-vars-scope="global"。实战一:把默认蓝换成品牌紫
步骤 1:全局定义设计令牌
在项目入口样式表里覆盖基础变量。注意选择器写成两个 :root 连写,这是官方推荐的优先级技巧——Vant 自身也在 :root 上声明变量,单写一个容易被优先级压住。
:root:root {
--van-primary-color: #7c3aed;
--van-success-color: #16a34a;
--van-radius-lg: 12px;
--van-text-color: #1f2937;
}
/* 指定组件变量,只影响主色按钮 */
:root:root {
--van-button-primary-background: #7c3aed;
--van-button-primary-border-color: #7c3aed;
}
步骤 2:局部区块用 themeVars 微调
属性名采用驼峰式,Vant 会自动转换成对应的 CSS 变量,例如 sliderBarHeight 会变成 --van-slider-bar-height。TypeScript 项目可以导入 ConfigProviderThemeVars 获得补全。
<script setup lang="ts">
import type { ConfigProviderThemeVars } from 'vant';
const themeVars: ConfigProviderThemeVars = {
rateIconFullColor: '#7c3aed',
sliderBarHeight: '4px',
sliderActiveBackground: '#7c3aed',
buttonPrimaryBackground: '#7c3aed',
buttonPrimaryBorderColor: '#7c3aed',
};
</script>
<template>
<van-config-provider :theme-vars="themeVars">
<van-form>
<van-field name="rate" label="评分">
<template #input><van-rate v-model="rate" /></template>
</van-field>
<van-button round block type="primary" native-type="submit">提交</van-button>
</van-form>
</van-config-provider>
</template>
实战二:一行切换暗黑模式
theme 属性只有 light 和 dark 两个取值,设为 dark 后会在根节点加上 .van-theme-dark,页面内所有 Vant 组件随之切换。把 theme 绑定成响应式变量,配合系统偏好与本地存储即可做成完整的换肤开关。
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { ConfigProviderTheme } from 'vant';
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = ref<ConfigProviderTheme>(
(localStorage.getItem('app-theme') as ConfigProviderTheme) ||
(prefersDark ? 'dark' : 'light')
);
watch(theme, (val) => {
localStorage.setItem('app-theme', val);
document.documentElement.dataset.theme = val;
}, { immediate: true });
</script>
<template>
<van-config-provider :theme="theme">
<van-nav-bar title="主题演示">
<template #right>
<van-switch :model-value="theme === 'dark'"
@update:model-value="theme = $event ? 'dark' : 'light'" />
</template>
</van-nav-bar>
<router-view />
</van-config-provider>
</template>
/* theme 只改组件,不改页面文字与背景,需要自己补 */
.van-theme-dark body {
color: #f5f5f5;
background-color: #111827;
}
/* 也可以按主题类单独定义基础变量 */
.van-theme-light { --van-background-2: #ffffff; }
.van-theme-dark { --van-background-2: #1f2937; }
theme 不会改变页面的 color 与 background-color。只写 theme="dark" 而不补上面这段 CSS,会出现「组件变黑、页面还是白底」的割裂效果。亮暗两套令牌:theme-vars-dark 与 theme-vars-light
当亮色和暗色需要不同的品牌色时,不必写两套 CSS。把通用变量放进 theme-vars,再分别用 theme-vars-dark / theme-vars-light 覆盖,后两者的优先级更高:
<script setup lang="ts">
import type { ConfigProviderThemeVars } from 'vant';
const themeVars: ConfigProviderThemeVars = { sliderBarHeight: '4px' };
const darkVars: ConfigProviderThemeVars = { buttonPrimaryBackground: '#7c3aed' };
const lightVars: ConfigProviderThemeVars = { buttonPrimaryBackground: '#1989fa' };
</script>
<template>
<van-config-provider
:theme="theme"
:theme-vars="themeVars"
:theme-vars-dark="darkVars"
:theme-vars-light="lightVars"
theme-vars-scope="global"
>
<router-view />
</van-config-provider>
</template>
常见坑速查
- local 模式改基础变量无效:见前文,改用
:root或global作用域。 - 覆盖不生效:检查是否被 Vant 的
:root声明压住,用:root:root提升优先级。 - 驼峰写错:
buttonPrimaryBackground会转成--van-button-primary-background,写成完整 CSS 变量名反而不会被转换。 - 弹层类组件脱离容器:Toast、Dialog、Popup 等挂载在 body 下,local 作用域的
themeVars覆盖不到它们,此时要用global或全局 CSS。 - 混用两套方案:不要一半用
:root覆盖、一半用theme-vars覆盖同一个变量,调试时很难判断最终值。
--van-white 这类调色板变量的场景。总结
Vant 4 的主题定制本质是一套带继承关系的 CSS 变量:先用基础变量对齐设计令牌,再用组件变量做局部精修,最后用 ConfigProvider 的 theme 与亮暗分令牌完成换肤。把层级分清楚,改版就从「逐个组件改样式」变成了「维护一张变量表」。
把视觉参数收敛成变量,主题才从一堆补丁变成一份配置。