首页
/ MUI CSS 主题变量实战:theme.vars、Channel Token 与自定义 Token 的完整使用指南

MUI CSS 主题变量实战:theme.vars、Channel Token 与自定义 Token 的完整使用指南

2026-09-06 11:44:32作者:伍希望

Material UI(MUI)通过 cssVariables 选项将主题令牌序列化为全局 CSS 自定义属性,使浏览器开发者工具中可以直接看到每个样式值对应的主题 token。本文基于仓库中的官方使用文档 usage.md 展开,并结合 mui-system 的 cssVars 模块 源码,完整讲解启用方式、深浅色模式行为、theme.varsvar() 的用法、颜色通道令牌(channel tokens)、自定义令牌扩展以及 TypeScript 类型启用步骤。读完后你可以:在应用中开启 CSS 主题变量、安全地为深色模式写样式、创建半透明颜色,并按需扩展自己的主题 token。

启用 CSS 主题变量

使用方式非常直接:创建主题时传入 cssVariables: true,并用 ThemeProvider 包裹应用:

import { ThemeProvider, createTheme } from '@mui/material/styles';

const theme = createTheme({ cssVariables: true });

function App() {
  return <ThemeProvider theme={theme}>{/* ...your app */}</ThemeProvider>;
}

渲染之后,你会在 HTML 文档的 :root 样式表中看到一组 CSS 变量。默认情况下这些变量是扁平化的,并以 --mui 作为前缀:

:root {
  --mui-palette-primary-main: #1976d2;
  --mui-palette-primary-light: #42a5f5;
  --mui-palette-primary-dark: #1565c0;
  --mui-palette-primary-contrastText: #fff;
  /* ...other variables */
}

如果你此前使用的是实验性的 CssVarsProvider API,现在应替换为 ThemeProviderCssVarsProvider 曾经提供的所有能力,如今都已由 ThemeProvider 承接。

源码层面:变量是如何注入页面的

从源码结构看,这条链路分为三步:

  1. 主题解析createTheme.tscssVariables 选项的签名是 boolean | Pick<CssVarsThemeOptions, CssVarsConfigList>,默认值为 false第 72 行)。当传入 true 或配置对象时,主题会被追加 colorSchemesvarsgenerateStyleSheets 等 CSS 变量基础设施;generateStyleSheetscreateCssVarsTheme.ts 赋值到主题输出上。
  2. 变量对象组装:Provider 在运行时计算 memoTheme,把 vars: themeVars 挂到主题上(themeVars 来自 generateThemeVars?.() || restThemeProp.vars),并把选定的 color scheme 一级合并进主题(createCssVarsProvider.js 第 155–191 行)。这就是后文 theme.vars 能镜像主题结构的原因。
  3. 样式表注入:Provider 最终渲染 <GlobalStyles styles={memoTheme.generateStyleSheets?.() || []} />第 314–323 行),把 :root(及深色方案选择器)下的变量声明写入全局样式表。遍历主题树、生成扁平变量与 channel token 的具体逻辑在 prepareCssVars.ts 中实现。

浅色与深色模式

当启用内置的深色 color scheme 且开启 cssVariables 时,浅色与深色的 CSS 变量会同时生成,并默认采用 CSS 媒体查询 prefers-color-scheme 方式切换。

这一方式的优点是:服务端渲染(SSR)下无需任何额外配置即可工作;缺点是用户无法手动切换模式,因为样式跟随浏览器媒体偏好。如果需要手动切换,需要把 colorSchemeSelector 改为 class 或 data 属性选择器,详见进阶配置文档中“手动切换深色模式”一节

从源码可以印证这套机制:createCssVarsTheme.ts 第 13 行colorSchemeSelector 的默认值是 [data-mui-color-scheme="%s"],而 Provider 会在色值方案变化时把该选择器解析为 class(如 .dark)或 data 属性(如 data-mui-color-scheme="dark")并写到 colorSchemeNode 上(createCssVarsProvider.js 第 196–237 行)。因此手动切换模式本质上是“改写根节点上的属性,让预先生成好的两套变量选择器命中不同的那份”。

应用深色样式

为深色模式定制样式时,请使用 theme.applyStyles() 函数(它在生成的样式表中输出对应 color scheme 选择器下的规则):

import Card from '@mui/material/Card';

<Card
  sx={[
    (theme) => ({
      backgroundColor: theme.vars.palette.background.default,
    }),
    (theme) =>
      theme.applyStyles('dark', {
        backgroundColor: theme.vars.palette.grey[900],
      }),
  ]}
/>;

注意:不要用 theme.palette.mode 在浅色/深色样式之间做条件判断——这会产生 SSR 闪烁(flicker)问题applyStyles() 生成的是纯 CSS 选择器规则,样式在 JS 执行前就已生效,而 palette.mode 的判断发生在 JS 运行时,两者行为本质不同。applyStyles 的类型定义见 createThemeFoundation.ts 第 433 行

使用主题变量

启用 CSS 变量功能后,主题上新增 vars 节点。vars 对象镜像了可序列化主题的结构,其中每个值都指向一个 CSS 变量,实际渲染为 var(--mui-...) 形式。

theme.vars(推荐)

const Button = styled('button')(({ theme }) => ({
  backgroundColor: theme.vars.palette.primary.main, // var(--mui-palette-primary-main)
  color: theme.vars.palette.primary.contrastText, // var(--mui-palette-primary-contrastText)
}));

对于 TypeScript,类型默认不启用,需要按后文 TypeScript 一节 完成模块增强。

如果组件可能渲染在 Provider 之外(此时 theme.vars 不存在),请加上回退:

backgroundColor: (theme.vars || theme).palette.primary.main;

原生 CSS

当你无法访问主题对象(例如在纯 CSS 文件中),直接用 var() 引用全局变量:

/* external-scope.css */
.external-section {
  background-color: var(--mui-palette-grey-50);
}

getCssVar:按字段名取值

主题还提供 getCssVar(field, ...fallbacks) 方法,免去手写前缀。其实现见 createGetCssVar.ts:将字段名拼成 var(--<prefix>-<field>),并对 fallback 值递归追加(仅当 fallback 不是原始颜色/数字值时才继续包一层 var())。类型声明位于 createThemeFoundation.ts 第 406 行,可用的字段名可通过 extendTheme.spec.ts 中的类型测试看到,例如 palette-primary-mainzIndex-appBarshape-borderRadius 等扁平化字段。

颜色通道令牌(Channel Tokens)

启用 cssVariables 会自动生成 channel token,用于创建半透明颜色。这些 token 由颜色空间通道组成、不含 alpha 分量、以空格分隔,命名上以 Channel 结尾:

const theme = createTheme({ cssVariables: true });

console.log(theme.palette.primary.mainChannel); // '25 118 210'
// 该 token 由 `theme.colorSchemes.light.palette.primary.main` 派生。

利用 channel token 可以很方便地构造半透明色:

const theme = createTheme({
  cssVariables: true,
  components: {
    MuiChip: {
      styleOverrides: {
        root: ({ theme }) => ({
          variants: [
            {
              props: { variant: 'outlined', color: 'primary' },
              style: {
                backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.12)`,
              },
            },
          ],
        }),
      },
    },
  },
});

注意:分隔符不能用逗号(,)。channel 颜色使用空格分隔通道,透明度部分以 / 连接(依据 CSS Color 4 规范的 modern 语法):

`rgba(${theme.vars.palette.primary.mainChannel}, 0.12)`, // 🚫 不能工作
`rgba(${theme.vars.palette.primary.mainChannel} / 0.12)`, // ✅ 始终使用 `/`

这正是 createGetCssVar.ts 第 14 行 正则中专门识别 \d+ \d+ \d+(三个空格分隔的通道数字)的原因:当值为 channel token 时,工具链知道它是可组合的原始值而非完整颜色字符串。

添加自定义主题令牌

你可以往主题输入中添加任意 key-value 对,它们会作为 CSS 主题变量的一部分生成。注意自定义令牌建议放在对应 color scheme 的 palette 下,且值中可以直接引用其他变量:

const theme = createTheme({
  cssVariables: true,
  colorSchemes: {
    light: {
      palette: {
        // 你可以在任意位置引用变量
        gradient:
          'linear-gradient(to left, var(--mui-palette-primary-main), var(--mui-palette-primary-dark))',
        border: {
          subtle: 'var(--mui-palette-neutral-200)',
        },
      },
    },
    dark: {
      palette: {
        gradient:
          'linear-gradient(to left, var(--mui-palette-primary-light), var(--mui-palette-primary-main))',
        border: {
          subtle: 'var(--mui-palette-neutral-600)',
        },
      },
    },
  },
});

function App() {
  return <ThemeProvider theme={theme}>...</ThemeProvider>;
}

随后可以从 theme.vars 对象访问这些变量:

const Divider = styled('hr')(({ theme }) => ({
  height: 1,
  border: '1px solid',
  borderColor: theme.vars.palette.border.subtle,
  backgroundColor: theme.vars.palette.gradient,
}));

或者用 var() 直接引用:

/* global.css */
.external-section {
  background-color: var(--mui-palette-gradient);
}

如果你使用了自定义前缀,记得把上面示例中的默认 --mui 替换为你的前缀。

从源码结构看,Provider 在组装 memoTheme 时会对选定 color scheme 做一级合并(第 171–190 行):palette 等对象型配置会被浅合并进主题。因此 light/dark 两套自定义令牌会同时进入 vars 输出,并在运行时按当前 color scheme 命中——这与内置 token 的行为完全一致。

TypeScript 类型启用

主题变量的类型默认不启用。你需要导入模块增强(module augmentation)来打开 theme.vars 的类型:

// 该 import 可以放在任何被 `tsconfig.json` 包含的文件中
import type {} from '@mui/material/themeCssVarsAugmentation';
import { styled } from '@mui/material/styles';

const StyledComponent = styled('button')(({ theme }) => ({
  // ✅ typed-safe
  color: theme.vars.palette.primary.main,
}));

增强模块对应仓库中的 packages/mui-material/src/themeCssVarsAugmentation/index.ts

扩展 Palette 接口

当往 palette 中添加新令牌(如上文的 gradientborder.subtle)时,还需要增强 PaletteOptionsPalette 两个接口,createTheme 的入参和 theme.vars.palette 才能通过类型检查:

declare module '@mui/material/styles' {
  interface PaletteOptions {
    gradient: string;
    border: {
      subtle: string;
    };
  }
  interface Palette {
    gradient: string;
    border: {
      subtle: string;
    };
  }
}

相关文档与延伸阅读

如果你需要同时支持系统偏好和手动选择,建议下一步阅读进阶配置文档

登录后查看全文
热门项目推荐
相关项目推荐