接手品牌改版时,最费力的往往不是写新页面,而是把一整套组件库的默认蓝一寸寸改掉。Vant ConfigProvider 主题定制正是为解决这类问题而生:它把散落在各组件里的视觉参数收敛成一套 --van-* CSS 变量,让品牌色替换、局部换肤、暗黑模式都能在一处完成。本文基于 Vant 4,从变量体系讲到 theme-vars 的作用域与亮暗双主题令牌,并给出可直接复制的主题切换代码。

先弄清 Vant 的 CSS 变量体系

Vant 4 的样式全部构建在 CSS 自定义属性上,变量分为基础变量与组件变量两类。组件变量默认继承基础变量,例如主色按钮的背景定义在 .van-button--primary 上,而它引用的又是指向调色板的基础变量:

css vant 默认变量(节选)
: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-colorvar(--van-blue)主色系组件(按钮、进度条、选择器确认按钮等)
--van-danger-colorvar(--van-red)错误态、删除类操作
--van-text-colorvar(--van-gray-8)全局主文本
--van-background-2var(--van-white)弹层、单元格等容器底色
--van-radius-lg8px卡片与弹层圆角
--van-padding-md16px组件通用内边距
--van-duration-base0.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 上声明变量,单写一个容易被优先级压住。

css src/styles/theme.css
: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 获得补全。

vue src/components/BrandForm.vue
<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 绑定成响应式变量,配合系统偏好与本地存储即可做成完整的换肤开关。

vue src/App.vue
<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>
css src/styles/theme.css
/* 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 不管页面背景
官方明确说明 theme 不会改变页面的 color 与 background-color。只写 theme="dark" 而不补上面这段 CSS,会出现「组件变黑、页面还是白底」的割裂效果。

亮暗两套令牌:theme-vars-dark 与 theme-vars-light

当亮色和暗色需要不同的品牌色时,不必写两套 CSS。把通用变量放进 theme-vars,再分别用 theme-vars-dark / theme-vars-light 覆盖,后两者的优先级更高:

vue src/App.vue(片段)
<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 与亮暗分令牌完成换肤。把层级分清楚,改版就从「逐个组件改样式」变成了「维护一张变量表」。

把视觉参数收敛成变量,主题才从一堆补丁变成一份配置。