Material UI 主题定制深度实践:createTheme、ThemeProvider 与 CSS 主题变量全解
本篇技术指南围绕 Material UI 仓库中的官方 Theming 文档(theming.md)展开,系统讲解如何用 createTheme 创建主题、用 ThemeProvider 将主题注入组件树、通过 useTheme 在组件内读取主题变量,以及启用 CSS 主题变量的完整方案。读完本文,你将能够独立完成一套符合品牌规范的 Material UI 主题配置,理解主题嵌套、className/style 合并等进阶行为,并掌握 responsiveFontSizes 与 enhanceHighContrast 两个主题增强函数的用法。
主题(Theme)的核心定位
主题规定了组件的颜色、表面的明暗程度、阴影层级、墨色元素的透明度等设计要素。通过主题,你可以为整个应用施加一致的视觉基调,定制化项目的所有设计层面,以满足业务或品牌的特定需求。
为了在不同应用之间获得更大的一致性,Material UI 提供 light 和 dark 两种主题类型供选择。默认情况下,组件使用 light 类型。
从源码结构看,主题的构建入口是 createTheme.ts。它在收到 options 后会做三件事:
- 解析
palette、cssVariables、colorSchemes、defaultColorScheme等顶层选项; - 若未启用 CSS 变量(
cssVariables为 false 且未声明colorSchemes),走与 v5 完全一致的 createThemeNoVars.js 分支,行为与历史版本兼容; - 若声明了
colorSchemes或启用了cssVariables,则进入 createThemeWithVars.js 分支,生成带 CSS 变量的主题,并处理 light/dark 双套色板(color scheme)的组装。
createThemeNoVars 内部会用 deepmerge 把用户传入的选项与默认主题合并,并逐项补全 mixins、palette、shadows、typography、transitions、zIndex 等缺失部分——这正是“传入不完整主题对象、自动补齐缺失部分”这一 API 承诺的实现基础。
ThemeProvider:把主题注入组件树
Material UI 组件开箱即贴合库的默认主题。要注入自定义主题,需要使用 ThemeProvider。它依赖 React 的 Context 机制把主题向下传递给组件,因此必须保证 ThemeProvider 是你要定制组件的祖先节点。
ThemeProvider 接收 theme prop,并将其应用到它所包裹的整个 React 子树;推荐把它放在组件树的根部。其 Props 如下:
| 名称 | 类型 | 说明 |
|---|---|---|
| children * | node | 你的组件树 |
| theme * | union: object | func | 主题对象,通常是 createTheme() 的结果。提供的主题会与默认主题合并。也可以提供一个函数来扩展外层主题 |
最小示例:
import * as React from 'react';
import { red } from '@mui/material/colors';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
main: red[500],
},
},
});
function App() {
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
除了文档列出的两个基础 prop,从 ThemeProvider.tsx 的源码定义可以看到,它在启用 CSS 主题变量时还暴露了一批实用 props:
| Prop | 默认值 | 作用 |
|---|---|---|
defaultMode |
'system' |
本地存储中尚无模式记录时的默认模式,要求主题定义了 light 和 dark 两套 colorSchemes |
modeStorageKey |
'mui-mode' |
存储应用模式(mode)的 localStorage key |
colorSchemeStorageKey |
'mui-color-scheme' |
存储 color scheme 的 localStorage key |
colorSchemeNode |
document |
挂载 theme.colorSchemeSelector 的节点 |
disableStyleSheetGeneration |
false |
禁止生成 CSS 主题变量样式表,用于控制嵌套 ThemeProvider 行为 |
disableNestedContext |
— | 若为 true,Provider 会像根 ThemeProvider 一样创建自己的上下文并生成样式表 |
forceThemeRerender |
false |
为 true 时,模式切换会重新计算主题值,theme.colorSchemes.{mode}.* 节点会被浅合并到主题顶层 |
noSsr |
false |
为 true 时 ThemeProvider 不重渲染,初始 mode 直接来自本地存储(SSR 需保证服务端输出与客户端首次渲染一致) |
disableTransitionOnChange |
false |
切换模式或 color scheme 时禁用 CSS 过渡 |
源码中还有一处值得注意的分支逻辑:若传入的主题不含 colorSchemes(即非 CSS 变量主题),ThemeProvider 会渲染 ThemeProviderNoVars,并对没有 vars 字段的普通主题强制补上 vars: null,目的是防止嵌套时从上层 CSS 变量主题错误地继承样式变量。
主题配置变量
修改主题配置变量是让 Material UI 贴合自身需求的最有效方式。官方文档覆盖的最重要主题变量包括:
.palette(颜色调色板).typography(排版).spacing(间距).breakpoints(断点).zIndex(层叠顺序).transitions(过渡).components(组件级定制:defaultProps、styleOverrides 等)
默认主题的全貌可以在仓库文档目录 docs/data/material/customization/ 下的默认主题(default theme)相关页面中查到。
自定义变量(Custom variables)
当你在主题中配合 MUI System 或其他样式方案使用时,往往需要在主题中添加额外的变量以便处处引用:
const theme = createTheme({
status: {
danger: orange[500],
},
});
需要特别警惕的是:vars 是为 CSS 主题变量自动生成的私有字段。如果你在 createTheme 中传入 vars 会直接抛出错误:
createTheme({
vars: { ... }, // ❌ error
})
这一点在源码中得到印证:createThemeNoVars.js 在检测到 options.vars 且 options.generateThemeVars === undefined 时,会抛出 MUI: \vars` is a private field used for CSS variables support.的错误。官方建议自定义对象另起他名(如文档示例中的status`,或警告信息中提示的“use another name”)。
TypeScript 类型增强
给 Theme 和 ThemeOptions 添加新变量,必须使用 TypeScript 的模块增强(module augmentation):
declare module '@mui/material/styles' {
interface Theme {
status: {
danger: string;
};
}
// allow configuration using `createTheme()`
interface ThemeOptions {
status?: {
danger?: string;
};
}
}
完整的可运行示例见 CustomStyles.js。若需要给 theme.palette 添加额外变量,则参见文档中的 palette 定制章节。
颜色操作工具
createTheme 返回的主题对象还会被 attachColorManipulators 附加三个工具方法:theme.alpha(color, coefficient)、theme.lighten(color, coefficient) 与 theme.darken(color, coefficient)。从源码结构看,当主题启用了 colorSpace 时,lighten/darken 会使用 color-mix(),alpha 会生成 oklch() 表达式;启用 CSS 变量时 alpha 则通过 rgba(var(--xxxChannel) / a) 的形式引用变量通道,保证颜色操作在变量模式下依然动态生效。
主题构建工具(Theme builder)
社区提供了若干可视化的主题构建工具,可以快速设计、预览和编辑主题:
- mui-theme-creator:帮助为 Material UI 设计并定制主题,附带基础站点模板,展示各组件受主题影响的效果;
- MUI Theme Builder:生成、预览和编辑 Material UI 主题的工具;
- Material palette generator:Material Design 官方的调色板生成器,可为任意输入颜色生成一套色阶。
在组件中访问主题:useTheme hook
在函数式组件中,可以用 useTheme hook 访问主题变量:
import { useTheme } from '@mui/material/styles';
function DeepChild() {
const theme = useTheme();
return <span>{`spacing ${theme.spacing}`}</span>;
}
从 useTheme.js 的实现看,它内部转调了 @mui/system 的 useTheme(defaultTheme),并以 defaultTheme 作为兜底默认值——所以即使没有包裹 ThemeProvider,useTheme 返回的也是完整的默认主题而非空对象。返回前它还会先查找内部 THEME_ID 键(theme[THEME_ID] || theme),以支持带内部标记的主题对象。
主题嵌套(Nesting the theme)
可以嵌套多个 ThemeProvider,内层主题会覆盖外层主题。仓库中的示例 ThemeNesting.js 展示了这一行为:外层主题把 primary.main 设为 orange[500],内层主题将其覆盖为 green[500],两个 Checkbox 分别呈现外、内两套主题色。
const outerTheme = createTheme({
palette: { primary: { main: orange[500] } },
});
const innerTheme = createTheme({
palette: { primary: { main: green[500] } },
});
export default function ThemeNesting() {
return (
<ThemeProvider theme={outerTheme}>
<Checkbox defaultChecked />
<ThemeProvider theme={innerTheme}>
<Checkbox defaultChecked />
</ThemeProvider>
</ThemeProvider>
);
}
如果希望扩展而不是整体替换外层主题,可以给 theme prop 传一个函数。ThemeNestingExtend.js 的完整实现:
const outerTheme = createTheme({
palette: {
secondary: {
main: orange[500],
},
},
});
export default function ThemeNestingExtend() {
return (
<ThemeProvider theme={outerTheme}>
<Checkbox defaultChecked color="secondary" />
<ThemeProvider
theme={(theme) =>
createTheme({
...theme,
palette: {
...theme.palette,
primary: {
main: green[500],
},
},
})
}
>
<Checkbox defaultChecked />
<Checkbox defaultChecked color="secondary" />
</ThemeProvider>
</ThemeProvider>
);
}
这里内层主题只改写了 primary,外层的 secondary(橙色)仍然保留——两个 checkbox 的 secondary 颜色一致,而内层的 primary 变为绿色。这与 ThemeProviderProps 中 theme: Partial<Theme> | ((outerTheme: Theme) => Theme) 的类型定义完全对应。
CSS 主题变量(cssVariables)
要基于主题生成 CSS 变量,在主题配置中把 cssVariables 设为 true 并传给 ThemeProvider:
const theme = createTheme({
cssVariables: true,
});
function App() {
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
这会生成一份包含 CSS 主题变量的全局样式表:
:root {
--mui-palette-primary-main: #1976d2;
/* ...other variables */
}
之后,ThemeProvider 下的所有组件都会使用这些 CSS 主题变量而不是原始值:
- color: #1976d2;
+ color: var(--mui-palette-primary-main);
从源码看,这条路径由 createTheme.ts 中 cssVariables !== false 的分支接管,最终交给 createThemeWithVars 生成;而 ThemeProvider.tsx 在检测到主题带有 colorSchemes 时会渲染 CssVarsProvider(来自 ThemeProviderWithVars.tsx),由它负责样式表生成与模式切换。这也解释了为何前文 ThemeProvider 的额外 props(modeStorageKey、colorSchemeNode 等)只在 CSS 变量模式下有意义。
API 详解
createTheme(options, ...args) => theme
根据传入的 options 生成主题,然后把它作为 prop 传给 ThemeProvider。
Arguments
options(object):接收一个不完整的主题对象,补齐缺失部分;...args(object[]):把后续参数与即将返回的主题做深合并。
注意:createTheme() 只处理第一个参数(options)。虽然出于向后兼容目前传多个参数也能工作,但该行为在未来版本可能被移除。为确保代码的前向兼容,建议手动深合并主题对象后以单个对象传入:
import { deepmerge } from '@mui/utils';
import { createTheme } from '@mui/material/styles';
const theme = createTheme(deepmerge(options1, options2));
Returns
theme(object):完整的、可直接使用的主题对象。
示例
import { createTheme } from '@mui/material/styles';
import { green, purple } from '@mui/material/colors';
const theme = createTheme({
palette: {
primary: {
main: purple[500],
},
secondary: {
main: green[500],
},
},
});
在 createTheme.ts 中可以看到,...args 确实被透传给了 createThemeNoVars/createThemeWithVars,并在内部通过 args.reduce((acc, argument) => deepmerge(acc, argument), muiTheme) 完成深合并——这就是多参数仍然“能工作”的实现来源。
主题组合:用主题选项定义其他选项
当某个主题选项的取值依赖于另一个主题选项时,应分步组合主题:
import { createTheme } from '@mui/material/styles';
let theme = createTheme({
palette: {
primary: {
main: '#0052cc',
},
secondary: {
main: '#edf2ff',
},
},
});
theme = createTheme(theme, {
palette: {
info: {
main: theme.palette.secondary.main,
},
},
});
可以把创建主题理解为两步组合过程:先定义基础设计选项,再用这些设计选项组合出其他选项。
警告:theme.vars 是用于 CSS 变量支持的私有字段,自定义对象请使用其他名称。
defaultProps 中 className 与 style 的合并(mergeClassNameAndStyle)
默认情况下,当组件在主题中定义了 defaultProps 时,直接传给组件的 props 会完全覆盖默认 props:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiButton: {
defaultProps: {
className: 'default-button-class',
style: { marginTop: 8 },
},
},
},
});
// className will be: "custom-button-class" (default ignored)
// style will be: { color: 'blue' } (default ignored)
<Button className="custom-button-class" style={{ color: 'blue' }}>
Click me
</Button>;
你可以把 theme.components.mergeClassNameAndStyle 配置为 true,让 className 和 style 走“合并”而非“替换”:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
mergeClassNameAndStyle: true,
MuiButton: {
defaultProps: {
className: 'default-button-class',
style: { marginTop: 8 },
},
},
},
});
该配置下的效果:
// className will be: "default-button-class custom-button-class"
// style will be: { marginTop: 8, color: 'blue' }
<Button className="custom-button-class" style={{ color: 'blue' }}>
Click me
</Button>
从源码结构看,该选项的类型声明位于 components.ts 中 components 选项的 mergeClassNameAndStyle?: boolean 字段,行为验证见 createTheme.spec.ts。
responsiveFontSizes(theme, options) => theme
根据传入的 options 生成响应式排版设置。
Arguments
theme(object):要增强的主题对象;options(object,可选):breakpoints(array<string>,可选):默认['sm', 'md', 'lg']。要处理的断点标识符数组;disableAlign(bool,可选):默认false。为 true 时字号变化幅度会略作调整,使行高保持在 Material Design 的 4px 行高网格上对齐;要求主题样式中使用无单位的行高;factor(number,可选):默认2。决定字号缩放强度:值越大,小屏上各字号差异越小;值越小,小屏字号越大。值必须大于 1;variants(array<string>,可选):默认处理全部排版变体。要处理的 typography 变体列表。
Returns
theme(object):带响应式排版的新主题。
示例
import { createTheme, responsiveFontSizes } from '@mui/material/styles';
let theme = createTheme();
theme = responsiveFontSizes(theme);
responsiveFontSizes.js 的实现细节与文档参数完全一致:默认 variants 列表包含 h1~h6、subtitle1/2、body1/2、caption、button、overline;字号按 minFontSize = 1 + (maxFontSize - 1) / factor 公式在小屏收敛到 1rem 基准;当行高非无单位且未设 disableAlign 时会抛出 Unsupported non-unitless line height with grid alignment 错误;并且实现中保留了“最大断点处字号回到原始设计值”的修正逻辑(对应上游 issue #40255)。
enhanceHighContrast(theme, tokens) => theme
为应用 @media (forced-colors: active) 覆盖规则的主题做增强,提升组件在 Windows 高对比度 / Forced Colors 模式下的可见性。自 v9.1.0 起可用。它接收一个已创建完成的主题并返回增强版本。
Arguments
theme(object):要增强的主题对象;tokens(object,可选):用于覆盖个别默认值的 CSS 系统颜色关键字对象。
| Token | 默认值 | 说明 |
|---|---|---|
disabled |
GrayText |
禁用元素颜色 |
error |
ActiveText |
错误状态颜色 |
selectedBackground |
SelectedItem |
选中项背景色 |
selectedText |
SelectedItemText |
选中项上的文字颜色 |
activeBackground |
Highlight |
激活/切换控件的背景色 |
activeText |
HighlightText |
激活/切换控件上的文字颜色 |
buttonBorder |
ButtonBorder |
交互控件的边框颜色 |
buttonText |
ButtonText |
按钮上的文字/图标颜色 |
canvas |
Canvas |
页面/画布背景色 |
Returns
theme(object):新主题,在受影响组件上应用了 forced-colors 覆盖。
示例
import { createTheme, enhanceHighContrast } from '@mui/material/styles';
// Use defaults
let theme = createTheme();
theme = enhanceHighContrast(theme);
// Override individual tokens
let theme = createTheme();
theme = enhanceHighContrast(theme, {
activeBackground: 'SelectedItem',
activeText: 'SelectedItemText',
});
enhanceHighContrast.ts 的实现比文档描述的更具体:它针对 MuiCheckbox、MuiRadio、MuiSwitch、MuiSlider、MuiMenuItem、MuiListItemButton、MuiButtonBase、MuiAutocomplete、MuiTooltip、MuiToggleButton、各类 Input 等约 20 个组件注入 @media (forced-colors: active) 的 styleOverrides。值得学习的实现手法有两点:一是使用数组形式的 styleOverrides 条目(如 root: [existing, newRule]),让 Emotion 把每个条目输出为独立 CSS 规则、交由浏览器级联解决优先级,避免直接覆盖用户已有的覆盖;二是对 Checkbox/Radio/Switch/Slider 这类“焦点或禁用元素是隐藏内部 input”的组件,用 :focus-within:has(input:focus-visible) 选择器恢复焦点轮廓,并借助 ownerState.disabled 处理不接收禁用类的 track/thumb 槽位。
unstable_createMuiStrictModeTheme(options, ...args) => theme
警告:不要在生产环境使用此方法。
生成一个减少 React.StrictMode 内警告(例如 Warning: findDOMNode is deprecated in StrictMode)的主题。
Requirements
目前 unstable_createMuiStrictModeTheme 不增加额外要求。
Arguments
options(object):接收不完整的主题对象并补齐缺失部分;...args(object[]):把参数与即将返回的主题深合并。
Returns
theme(object):完整的、可直接使用的主题对象。
示例
import { unstable_createMuiStrictModeTheme } from '@mui/material/styles';
const theme = unstable_createMuiStrictModeTheme();
function App() {
return (
<React.StrictMode>
<ThemeProvider theme={theme}>
<LandingPage />
</ThemeProvider>
</React.StrictMode>
);
}
其实现位于 createMuiStrictModeTheme.js。
小结与实践建议
- 主题入口是
createTheme,注入靠ThemeProvider,读取用useTheme,三者分别对应仓库中的 createTheme.ts、ThemeProvider.tsx、useTheme.js; - 传多参数给
createTheme属于待移除的兼容行为,新代码应使用deepmerge手工合并后传单对象; vars是保留字段,自定义变量请另起命名并用 TypeScript 模块增强补齐类型;- 嵌套
ThemeProvider时,传函数形式theme可以基于外层主题做增量修改,避免内层主题把外层配置整体覆盖掉; - 需要运行时切换 light/dark 且希望切换即时生效时,优先考虑
cssVariables: true路径; - 生产环境请避免使用
unstable_前缀的 API;涉及高对比度无障碍需求时,可在 v9.1.0+ 中用enhanceHighContrast一步完成 forced-colors 适配。
更多配套文档可参考官方 Theming 文档源文件 theming.md,以及文档数据目录下的 ThemeNesting.js 与 ThemeNestingExtend.js 两个可直接运行的示例。
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 StartedRust0624
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