首页
/ MUI System CSS 主题变量实验性 API:unstable_createCssVarsProvider 深度解析与实战

MUI System CSS 主题变量实验性 API:unstable_createCssVarsProvider 深度解析与实战

2026-09-06 17:53:39作者:尤辰城Agatha

CSS 主题变量(CSS theme variables)是 MUI System 在 v5.0.5 中以实验性导出(experimental export)形式引入的功能,它让底层 Material UI 或任何自定义 UI 组件在渲染时使用生成的 CSS 变量(如 var(--my-app-palette-primary-default))而非原始值,从而在构建时即可将主题注入应用样式表,在整棵 React 树渲染前应用用户选定的配色方案。本文基于仓库中的实验性 API 文档 css-theme-variables.md 及其对应演示 CreateCssVarsProvider.tsx,结合 @mui/system 源码中的 createCssVarsProvider.jsprepareCssVars.tscreateGetCssVar.ts,完整讲解其设计动机、性能权衡、完整用法与 API 选项。读完本文,你将掌握如何用 unstable_createCssVarsProviderunstable_prepareCssVarsunstable_createGetCssVar 从零搭建一套支持亮/暗切换、无 SSR 闪烁的 CSS 变量主题系统。

一、为什么需要 CSS 主题变量:优势与代价

CSS 自定义属性(CSS Custom Properties)是跨浏览器的现代特性,允许你在 CSS 中声明变量并在其他属性中复用。Material UI 在其默认主题方案之外,将主题值序列化为 CSS 变量后,带来了以下文档中列出的具体优势:

  • 消除暗色模式 SSR 闪烁:传统方案下,服务端渲染输出亮色 HTML,客户端 JS 执行后才切换为暗色,产生可见闪烁。CSS 变量方案通过在 <head> 注入一段内联脚本,在 React 挂载前就设置好根节点上的颜色方案标记,从根源上避免这一问题。
  • 支持无限颜色方案:除了 lightdark,你还可以定义如 high-contrastsepia 等任意数量的 color scheme。
  • 更好的调试体验:开发者与设计师都能在浏览器 DevTools 中直接看到 --my-app-palette-primary-default 这类变量名及其取值。
  • 浏览器标签页间自动同步:颜色方案选择通过 localStorage 存储,多标签页打开时自动保持一致。
  • 简化第三方工具集成:CSS 变量在全局作用域可用,任何第三方库都能直接引用。
  • 减少嵌套主题(nested theme)需求:当只想对应用局部应用暗色样式时,无需再包裹一层 ThemeProvider,直接在局部节点切换 data 属性即可。

对于服务端应用,文档同时明确列出性能权衡(trade-offs):

指标 与默认方案相比 原因
HTML 体积 更大 CSS 变量在构建时为 light 和 dark 两种模式同时生成
首次内容绘制(FCP) 更慢 HTML 体积增大,下载与解析耗时略增
可交互时间(TTI) 暗色模式下更快 切换亮/暗时样式表无需重新生成,执行 JS 的时间大幅减少

文档同时警告:上述对比在大型、复杂应用中未必成立,因为性能指标受众多因素影响。

二、核心函数总览:unstable_ 前缀的三个导出

@mui/system 的入口 index.js 中,CSS 变量相关的实验性导出如下:

// packages/mui-system/src/index.js(节选)
export { default as unstable_createCssVarsProvider } from './cssVars/createCssVarsProvider';
export { default as unstable_createGetCssVar } from './cssVars/createGetCssVar';
export { default as unstable_prepareCssVars } from './cssVars/prepareCssVars';
导出名 源码位置 作用
unstable_createCssVarsProvider createCssVarsProvider.js 高阶函数,接收主题配置,返回 { CssVarsProvider, useColorScheme, getInitColorSchemeScript } 三件套
unstable_prepareCssVars prepareCssVars.ts colorSchemes 解析为 vars 对象和 generateStyleSheets / generateThemeVars 两个函数
unstable_createGetCssVar createGetCssVar.ts 根据前缀生成 getCssVar(field, ...fallbacks) 函数,用于在主题内部引用 CSS 变量

文档特别指出:如果你在 Material UI 中使用,可以直接使用其暴露的 CssVarsProvider 组件(完整用法见 ThemeProviderWithVars.tsx),无需手动配置上述底层函数。但理解底层 API 对于构建自定义设计系统或框架无关的主题方案至关重要。

三、从零构建 CSS 变量主题:extendTheme 完整示例

以下示例直接继承自文档 css-theme-variables.md 中的 extendTheme.js 部分,并结合演示文件 CreateCssVarsProvider.tsx 的 TypeScript 类型定义进行了注释补充。

// extendTheme.js
import {
  unstable_createGetCssVar as systemCreateGetCssVar,
  unstable_prepareCssVars as prepareCssVars,
} from '@mui/system';

// 定义亮色方案(light color scheme)
const lightColorScheme = {
  palette: {
    mode: 'light',
    primary: {
      default: '#3990FF',
      dark: '#02367D',
    },
    text: {
      default: '#111111',
    },
    // ... other colors
  },
};

// 定义暗色方案(dark color scheme)
const darkColorScheme = {
  palette: {
    mode: 'dark',
    primary: {
      default: '#265D97',
      dark: '#132F4C',
      main: '#5090D3',
    },
    text: {
      default: '#ffffff',
    },
    // ... other colors
  },
};

// 创建 getCssVar 工厂函数,前缀为 'my-app'
const createGetCssVar = (cssVarPrefix = 'my-app') =>
  systemCreateGetCssVar(cssVarPrefix);

function extendTheme({ cssVarPrefix = 'my-app' } = {}) {
  const getCssVar = createGetCssVar(cssVarPrefix);

  const theme = {
    colorSchemes: {
      light: lightColorScheme,
      dark: darkColorScheme,
    },
    // ... any other objects independent of color-scheme,
    // like fontSizes, spacing tokens, etc.
  };

  // 核心:调用 prepareCssVars 将 colorSchemes 转换为
  // vars 对象(CSS 变量引用)和 generateCssVars 函数(生成样式表)
  const { vars: themeVars, generateCssVars } = prepareCssVars(
    { colorSchemes: theme.colorSchemes },
    {
      prefix: cssVarPrefix,
    },
  );

  theme.vars = themeVars;              // 供组件通过 theme.vars.xxx 引用
  theme.generateCssVars = generateCssVars; // 供 CssVarsProvider 生成 <style> 标签
  theme.palette = {
    ...theme.colorSchemes.light.palette,
    colorScheme: 'light',
  };

  return theme;
}

const myCustomDefaultTheme = extendTheme();

export default myCustomDefaultTheme;

prepareCssVars 的底层实现

prepareCssVars.ts 源码可以看到,该函数接收两个参数:

  • theme:包含 colorSchemes 键的主题对象,各方案结构须一致。
  • parserConfig:可选配置,支持以下字段:
    • prefix:CSS 变量前缀,如 'my-app',最终生成 --my-app-palette-primary-default
    • colorSchemeSelector:控制 CSS 变量的作用域选择器,取值为 'media' | 'class' | 'data' | string
      • 'media':使用 @media (prefers-color-scheme: dark) 媒体查询(默认值,此时 setMode 无效,因为切换依赖系统偏好)。
      • 'class':在根节点切换 CSS 类名,如 .dark
      • 'data':在根节点切换 data 属性,如 [data-color-scheme="dark"]
      • 自定义字符串如 'data-my-app-color-scheme':生成 [data-my-app-color-scheme="dark"] 选择器。
    • disableCssColorScheme:是否跳过自动设置 CSS color-scheme 属性。
    • enableContrastVars:是否启用对比度辅助变量(--__l-threshold--__l--__a)。
    • getSelector:自定义选择器生成函数,可完全替代默认的 defaultGetSelector 逻辑。
    • shouldSkipGeneratingVar:回调函数,按对象路径决定是否跳过生成某个变量。

函数返回三个成员:

  • vars:CSS 变量引用对象,如 { palette: { primary: { default: 'var(--my-app-palette-primary-default)' } } }
  • generateThemeVars():重新生成合并了所有 color scheme 的 vars 对象。
  • generateStyleSheets():返回 CSS 规则对象数组,供 GlobalStyles 渲染为 <style> 标签,每个 color scheme 对应一组选择器规则。

prepareCssVars.ts 第 56-91 行defaultGetSelector 实现可以看到:

  • selector'media' 且当前 color scheme 是默认方案时,直接返回 ':root';否则返回 @media (prefers-color-scheme: {mode}) { :root: {...} } 嵌套对象。
  • selector'class' 时,规则为 .%s,默认方案额外附加 :root, .{scheme}
  • selector'data' 时,规则为 [data-%s]
  • selector'data-xxx' 形式时,自动转换为 [data-xxx="%s"]

createGetCssVar 的实现细节

createGetCssVar.ts 返回的 getCssVar 函数接受一个主字段名和若干 fallback 值:

const getCssVar = createGetCssVar('my-app');
getCssVar('palette.primary.default');
// => 'var(--my-app-palette-primary-default)'

getCssVar('boxShadow.md', '0 4px 8px rgba(0,0,0,0.2)');
// => 'var(--my-app-box-shadow-md, 0 4px 8px rgba(0,0,0,0.2))'

源码中有一个关键细节(createGetCssVar.ts 第 11-19 行):如果 fallback 值匹配了颜色、长度单位或纯数字的正则,则直接作为 CSS fallback 值追加;否则也作为 fallback 追加。这使得 getCssVar 在主题变量尚未注入时也能提供合理的回退值。

四、创建 CssVarsProvider:高阶函数调用

创建 Provider 需要调用 unstable_createCssVarsProvider 高阶函数,文档中的 CssVarsProvider.js 示例如下:

// CssVarsProvider.js
import { unstable_createCssVarsProvider as createCssVarsProvider } from '@mui/system';

const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } =
  createCssVarsProvider({
    defaultColorScheme: {
      light: 'light',
      dark: 'dark',
    },
    theme: myCustomDefaultTheme,
  });

export { CssVarsProvider, useColorScheme, getInitColorSchemeScript };

createCssVarsProvider.js 第 18-33 行 可以确认该高阶函数接收的全部选项参数:

参数 类型 默认值 说明
themeId string undefined 多设计系统共存时的唯一标识,用于从 theme[themeId] 中取出对应的子主题
theme object {} 设计系统默认主题,必须包含 colorSchemes
modeStorageKey string 'mode' 存储用户选择 mode(light/dark/system)的 localStorage 键名
colorSchemeStorageKey string 'color-scheme' 存储 colorSchemelocalStorage 键名
disableTransitionOnChange boolean false 切换模式时禁用 CSS 过渡动画
defaultColorScheme string | { light, dark } 设计系统默认颜色方案,单方案传字符串,双方案传对象
resolveTheme (theme) => Theme undefined CSS 变量附加后调用的回调,返回值传入 ThemeProvider

该函数返回三项(createCssVarsProvider.js 第 413 行):

return { CssVarsProvider, useColorScheme, getInitColorSchemeScript };

CssVarsProvider 的 Props

文档中列出的核心 props 与源码 createCssVarsProvider.d.ts 中的类型定义一致:

  • defaultMode?: 'light' | 'dark' | 'system':应用默认模式,'light' 为文档默认值,源码中实际默认为 'system'createCssVarsProvider.js 第 70 行)。
  • disableTransitionOnChange: boolean:切换模式时禁用 CSS 过渡。
  • theme: ThemeInput:传入 React Context 的主题对象,须包含:
    • colorSchemes: { [key: string]: ColorScheme }
    • colorSchemeSelector: 'media' | 'class' | 'data' | string
    • generateStyleSheets: () => Record<string, string>
    • generateThemeVars: () => Record<string, any>
  • modeStorageKey?: stringlocalStorage 键名。
  • 源码中额外支持的 props(未在文档 API 节完整列出但存在于 createCssVarsProvider.js 第 57-73 行):
    • colorSchemeStorageKey:存储 colorScheme 的 localStorage 键。
    • colorSchemeNode:挂载 color-scheme 属性的 DOM 节点,默认 document.documentElement
    • documentNode:用于 disableTransitionOnChange 的 document 节点。
    • storageManager:自定义存储管理器,替代 window.localStorage
    • storageWindow:监听 'storage' 事件的 window 引用。
    • disableNestedContext:嵌套 Provider 时是否创建独立 context。
    • disableStyleSheetGeneration:是否跳过样式表生成。
    • forceThemeRerender:模式切换时是否重新计算主题值。
    • noSsr:与 InitColorSchemeScript 配合使用,避免水合后额外重渲染。

useColorScheme Hook

function App() {
  const { setMode, mode } = useColorScheme();
  const toggleMode = () => {
    setMode(mode === 'dark' ? 'light' : 'dark');
  };
  // ...
}

useCurrentColorScheme.tsResult 类型可以推断,useColorScheme 返回的上下文值包含:

  • mode: string:用户当前选择的模式。
  • setMode: (mode: string | null) => void:设置模式,值保存到内部 state 和 localStorage;传 null 时重置为默认模式。
  • colorScheme:当前生效的颜色方案名称。
  • setColorScheme:设置颜色方案(当 color scheme 与 mode 分离时使用)。
  • systemMode:系统偏好模式(light 或 dark)。
  • allColorSchemes:所有已注册的 color scheme 名称数组。

getInitColorSchemeScript

文档 API 节列出了 getInitColorSchemeScript: (options) => React.ReactElement,其 options 为:

  • defaultMode?: 'light' | 'dark' | 'system':React 渲染树之前的默认模式,'light' 为默认值。
  • modeStorageKey?: string:存储 mode 的 localStorage 键名。
  • attribute?: string:用于应用颜色方案的 DOM 属性名。

createCssVarsProvider.js 第 404-411 行 可以看到,该函数实际委托给 buildInitColorSchemeScript(来自 InitColorSchemeScript.tsx),生成一段内联 <script> 元素,在 React 挂载前执行,从 localStorage 读取用户偏好并设置根节点属性,从而避免 SSR 闪烁。

五、组件中使用 CSS 变量

文档中的 Button.js 示例展示了在 styled 组件中引用 theme.vars

// Button.js
import { styled } from '@mui/system';

const Button = styled('button')(({ theme }) => ({
  backgroundColor: theme.vars.palette.primary.default,
  border: `1px solid ${theme.vars.palette.primary.dark}`,
  color: theme.vars.palette.text.default,
}));

export default Button;

演示文件 CreateCssVarsProvider.tsx 第 91-98 行 中,按钮还额外使用了 [data-system-demo-color-scheme="dark"] & 选择器来覆盖包裹容器的背景色,这展示了在自定义 colorSchemeSelector 下,如何针对特定方案编写条件样式。

完整的应用入口:

// App.js
function App() {
  const { setMode, mode } = useColorScheme();
  const toggleMode = () => {
    setMode(mode === 'dark' ? 'light' : 'dark');
  };

  return (
    <div>
      <h1>Current Mode: {mode}</h1>
      <Button onClick={toggleMode}>Toggle Mode</Button>
    </div>
  );
}

// main.js
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './App';
import { CssVarsProvider } from './CssVarsProvider';

ReactDOM.createRoot(document.getElementById('root')).render(
  <CssVarsProvider>
    <App />
  </CssVarsProvider>,
);

切换模式后,ButtonbackgroundColorborderColor 和文本 color 会自动使用对应 mode 的颜色值,因为底层 CSS 变量值在根节点属性切换后由浏览器即时解析。

六、CssVarsProvider 内部工作机制

createCssVarsProvider.js 源码可以梳理出 CssVarsProvider 的完整渲染流程:

  1. 解析主题(第 79-91 行):支持传入 theme 作为 prop 或函数(() => theme),并处理 themeId 场景下从 theme[themeId] 中提取子主题。

  2. 获取 mode 与 colorScheme 状态(第 110-128 行):调用内部 useCurrentColorScheme Hook,从 localStorage 和系统偏好中计算出当前 modecolorSchemesystemMode 及对应的 setModesetColorScheme 函数。

  3. 嵌套 Provider 检测(第 133-136 行):如果检测到上层已有同前缀的 ColorSchemeContext,则继承上层的 modecolorScheme,避免重复生成样式表。

  4. 合成主题对象(第 155-191 行):

    • 调用 generateThemeVars?.() 或读取 restThemeProp.vars 获取 CSS 变量引用对象。
    • colorSchemescomponentscssVarPrefixvars 合并到主题顶层。
    • 若主题包含 generateSpacing 函数,自动调用生成 spacing 函数。
    • 将当前 colorScheme 对应的方案对象浅合并到主题顶层(第 172-187 行)。
    • 最后调用 resolveTheme(theme) 返回最终主题(如果提供了该回调)。
  5. 设置 colorSchemeSelector 属性(第 195-237 行):根据 colorSchemeSelector 的值,在 colorSchemeNode(默认 document.documentElement)上切换 class 或 data 属性,使 CSS 中对应选择器的变量值生效。

  6. 禁用过渡动画(第 241-258 行):当 disableTransitionOnChangetrue 且非首次挂载时,临时插入一段 *{transition:none!important} 样式,强制浏览器重绘后 1ms 移除,实现瞬时切换(此技巧借鉴自 next-themes 项目)。

  7. 渲染输出(第 314-331 行):

    • 始终渲染 <ThemeProvider theme={memoTheme}> 包裹 children。
    • 当满足样式表生成条件时,额外渲染 <GlobalStyles styles={memoTheme.generateStyleSheets?.() || []} />,将所有 color scheme 的 CSS 变量规则注入 DOM。
    • 非嵌套场景下,外层包裹 <ColorSchemeContext.Provider>

样式表生成条件(第 305-312 行):当 disableStyleSheetGeneration 为 true、主题中 cssVariables === false、或嵌套且上层已有同 cssVarPrefix 的 Provider 时,跳过生成。

七、与 Material UI 的衔接

文档末尾指出,Material UI 对 createCssVarsProvider 的完整使用可参考 ThemeProviderWithVars.tsx。在 Material UI 中,你无需手动调用 unstable_createCssVarsProvider,而是直接使用其暴露的 CssVarsProvider 组件,主题通过 extendTheme 自动处理 colorSchemesvarsgenerateStyleSheets 等字段的生成。

对于框架或语言特定的设置指南(如 Next.js 的 getInitColorSchemeScript 集成、Vite 的 SSR 配置等),文档引导读者参考 Material UI 文档站中 CSS theme variables 的 Usage 章节(路径为 /material-ui/customization/css-theme-variables/usage/)。

八、关键 API 速查表

unstable_createCssVarsProvider 选项

选项 类型 说明
modeStorageKey? string 存储 mode 的 localStorage 键,默认 'mode'
colorSchemeStorageKey? string 存储 colorScheme 的 localStorage 键,默认 'color-scheme'
defaultColorScheme string | { light, dark } 设计系统默认颜色方案
defaultMode? 'light' | 'dark' | 'system' 设计系统默认模式,默认 'light'(文档)/ 'system'(源码)
disableTransitionOnChange? boolean 切换时禁用 CSS 过渡,默认 false
themeId? string 多设计系统共存时的唯一标识
theme object 设计系统默认主题
resolveTheme? (theme) => Theme CSS 变量附加后调用的主题解析回调

返回值

成员 类型 说明
CssVarsProvider React 组件 主题 Provider,包裹应用顶层
useColorScheme Hook 返回 { mode, setMode, colorScheme, setColorScheme, systemMode, allColorSchemes }
getInitColorSchemeScript (options) => ReactElement 生成防闪烁内联脚本,用于 SSR 场景

prepareCssVars 配置

参数 类型 说明
prefix string CSS 变量前缀
colorSchemeSelector 'media' | 'class' | 'data' | string 颜色方案应用方式
disableCssColorScheme boolean 是否跳过设置 CSS color-scheme 属性
enableContrastVars boolean 是否启用对比度辅助变量
shouldSkipGeneratingVar (keys, value) => boolean 按路径跳过特定变量生成
getSelector (colorScheme, css) => string | object 自定义选择器生成函数

本文基于 css-theme-variables.md 文档的完整内容,并结合 CreateCssVarsProvider.tsx 演示、createCssVarsProvider.js 源码实现、prepareCssVars.tscreateGetCssVar.ts 底层逻辑进行了纵深扩充。注意该 API 带有 unstable_ 前缀,属于实验性接口,在未来版本中可能有 breaking changes,生产环境使用前建议关注 MUI 的发布说明。

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