MUI System CSS 主题变量实验性 API:unstable_createCssVarsProvider 深度解析与实战
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.js、prepareCssVars.ts 与 createGetCssVar.ts,完整讲解其设计动机、性能权衡、完整用法与 API 选项。读完本文,你将掌握如何用 unstable_createCssVarsProvider、unstable_prepareCssVars、unstable_createGetCssVar 从零搭建一套支持亮/暗切换、无 SSR 闪烁的 CSS 变量主题系统。
一、为什么需要 CSS 主题变量:优势与代价
CSS 自定义属性(CSS Custom Properties)是跨浏览器的现代特性,允许你在 CSS 中声明变量并在其他属性中复用。Material UI 在其默认主题方案之外,将主题值序列化为 CSS 变量后,带来了以下文档中列出的具体优势:
- 消除暗色模式 SSR 闪烁:传统方案下,服务端渲染输出亮色 HTML,客户端 JS 执行后才切换为暗色,产生可见闪烁。CSS 变量方案通过在
<head>注入一段内联脚本,在 React 挂载前就设置好根节点上的颜色方案标记,从根源上避免这一问题。 - 支持无限颜色方案:除了
light和dark,你还可以定义如high-contrast、sepia等任意数量的 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:是否跳过自动设置 CSScolor-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' |
存储 colorScheme 的 localStorage 键名 |
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' | stringgenerateStyleSheets: () => Record<string, string>generateThemeVars: () => Record<string, any>
modeStorageKey?: string:localStorage键名。- 源码中额外支持的 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.ts 的 Result 类型可以推断,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>,
);
切换模式后,Button 的 backgroundColor、borderColor 和文本 color 会自动使用对应 mode 的颜色值,因为底层 CSS 变量值在根节点属性切换后由浏览器即时解析。
六、CssVarsProvider 内部工作机制
从 createCssVarsProvider.js 源码可以梳理出 CssVarsProvider 的完整渲染流程:
-
解析主题(第 79-91 行):支持传入
theme作为 prop 或函数(() => theme),并处理themeId场景下从theme[themeId]中提取子主题。 -
获取 mode 与 colorScheme 状态(第 110-128 行):调用内部
useCurrentColorSchemeHook,从 localStorage 和系统偏好中计算出当前mode、colorScheme、systemMode及对应的setMode、setColorScheme函数。 -
嵌套 Provider 检测(第 133-136 行):如果检测到上层已有同前缀的
ColorSchemeContext,则继承上层的mode和colorScheme,避免重复生成样式表。 -
合成主题对象(第 155-191 行):
- 调用
generateThemeVars?.()或读取restThemeProp.vars获取 CSS 变量引用对象。 - 将
colorSchemes、components、cssVarPrefix、vars合并到主题顶层。 - 若主题包含
generateSpacing函数,自动调用生成spacing函数。 - 将当前
colorScheme对应的方案对象浅合并到主题顶层(第 172-187 行)。 - 最后调用
resolveTheme(theme)返回最终主题(如果提供了该回调)。
- 调用
-
设置 colorSchemeSelector 属性(第 195-237 行):根据
colorSchemeSelector的值,在colorSchemeNode(默认document.documentElement)上切换 class 或 data 属性,使 CSS 中对应选择器的变量值生效。 -
禁用过渡动画(第 241-258 行):当
disableTransitionOnChange为true且非首次挂载时,临时插入一段*{transition:none!important}样式,强制浏览器重绘后 1ms 移除,实现瞬时切换(此技巧借鉴自 next-themes 项目)。 -
渲染输出(第 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 自动处理 colorSchemes、vars、generateStyleSheets 等字段的生成。
对于框架或语言特定的设置指南(如 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.ts 与 createGetCssVar.ts 底层逻辑进行了纵深扩充。注意该 API 带有 unstable_ 前缀,属于实验性接口,在未来版本中可能有 breaking changes,生产环境使用前建议关注 MUI 的发布说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00