Material UI 默认主题全解析:createTheme() 的组装机制与默认主题查看器
本文围绕 Material UI(MUI)官方文档中的「Default theme viewer」页面展开,深入讲解默认主题对象是如何由 createTheme() 逐层组装出来的:包括 palette、typography、shadows、transitions、zIndex、motion 等顶层模块的默认值来源,以及文档站内那棵可交互的主题树(Theme Viewer)的实现原理。读完本文,你将能在浏览器控制台里完整探索默认主题对象、理解 colorSchemes / cssVariables 的新特性,并能通过 ?expand-path= 查询参数精准定位任意主题节点。
文档页在讲什么:一棵可探索的默认主题树
官方文档的 default-theme 页面(default-theme.md)本身只有一段简短说明:
This tree view allows you to explore how the theme object looks like with the default values.
它提供了一个树状视图(tree view),让开发者直接"看见" createTheme() 不传任何参数时生成的完整主题对象长什么样。文档同时给出三个关键指引:
- 主题如何组装:去看
packages/mui-material/src/styles/createTheme.ts及其相关的导入模块——createTheme()内部依次调用createPalette、createTypography、createTransitions、createMixins等工厂函数拼装出完整对象; - 在浏览器里动手:文档站点在所有文档页面都暴露了
theme变量(即window.theme),可以直接在控制台把玩文档站使用的主题对象; - 一个重要警告:文档站点使用的是 MUI 组织品牌定制主题(custom theme),因此页面上看到的
window.theme并不是纯默认主题,而是一棵被 MUI 品牌色覆盖过的树。
页面的主体是一个内嵌 demo:DefaultTheme.js。下面逐层拆解这个 demo 与它背后引用的主题实现源码。
createTheme() 的组装入口:createTheme.ts
默认主题的入口实现在 createTheme.ts。它的签名是 createTheme(options, ...args),其中 ...args 会被深度合并进最终主题,这一行为在 createThemeNoVars.js 中由 args.reduce((acc, argument) => deepmerge(acc, argument), muiTheme) 一行完成。
入口函数根据 cssVariables 选项分成三条路径:
// createTheme.ts(节选)
const {
palette,
cssVariables = false,
colorSchemes: initialColorSchemes = !palette ? { light: true } : undefined,
defaultColorScheme: initialDefaultColorScheme = palette?.mode,
...other
} = options;
cssVariables === false且未显式传入colorSchemes:直接走createThemeNoVars(options),行为"与 v5 完全一致"(源码注释原话// Behaves exactly as v5)。这就是默认路径,也就是默认主题查看器展示的那棵树;cssVariables === false但显式传了colorSchemes:在 NoVars 主题上手动挂defaultColorScheme/colorSchemes,并为每个 scheme 用attachColorScheme()生成独立palette,再逐 scheme 解析focusVisible外框颜色;cssVariables !== false:走createThemeWithVars(),主题值会进一步被编译为 CSS 变量(theme.vars),供运行时切换深浅色模式使用。
可以推断:新版 MUI 把"单色板主题"重构成了"多 colorScheme 主题"——palette.mode 只是第一个(默认)scheme 的输入,colorSchemes 允许一个主题对象同时携带 light / dark 两套完整调色板。
默认主题的逐模块构成:createThemeNoVars.js
真正拼装那棵默认主题树的是 createThemeNoVars.js:
let muiTheme = deepmerge(systemTheme, {
mixins: createMixins(systemTheme.breakpoints, mixinsInput),
palette,
shadows: shadows.slice(),
typography: createTypography(palette, typographyInput),
motion: createMotion(motionInput),
transitions: createTransitions(transitionsInput),
zIndex: { ...zIndex },
});
注意第一层是 systemTheme——它来自 @mui/system/createTheme(options),提供了 Material 之外更底层的 breakpoints、spacing、shape、components、unstable_sxConfig 等字段。随后 deepmerge 叠加 Material 专属模块,最后再合并用户传入的 other 字段。也就是说,一棵完整默认主题的顶层字段 = System 基础层 + Material 定制层 + 用户覆盖层三层深度合并的产物。
palette:调色板
palette 由 createPalette() 生成。不传任何选项时,MUI 使用 Material Design 的经典蓝色(primary.main 为 #1976d2)以及一套 grey、success、error、warning、info 等语义色阶,并自动推导 contrastText 等对比色字段。文档中 palette.md 引导读者"用主题查看器探索默认调色板",其链接正是指向本页并附带 ?expand-path=$.palette 参数。
shadows:25 级阴影
默认阴影表定义在 shadows.js:一个长度为 25 的数组,索引 0 是 'none',索引 1~24 由 createShadow() 生成,每级阴影都由三层叠加而成——
- 关键影(umbra),透明度 0.2;
- 关键半影(penumbra),透明度 0.14;
- 环境影(ambient),透明度 0.12。
数值来源注释明确标注取自 material-components-web 的 elevation 变量(mdc-elevation/_variables.scss),因此 theme.shadows[3] 这类取值与 Material 设计规范中的 elevation 等级一一对应。
transitions:动画时长与缓动
createTransitions.js 提供四组默认缓动曲线和七档默认时长:
| 缓动 | 曲线 | 适用场景(源码注释) |
|---|---|---|
easeInOut |
cubic-bezier(0.4, 0, 0.2, 1) |
最常用的通用曲线 |
easeOut |
cubic-bezier(0.0, 0, 0.2, 1) |
元素从屏外进入、减速停在原位 |
easeIn |
cubic-bezier(0.4, 0, 1, 1) |
元素离开屏幕,不减速 |
sharp |
cubic-bezier(0.4, 0, 0.6, 1) |
可能随时返回屏幕的对象 |
| 时长 | 毫秒 | 定位 |
|---|---|---|
shortest |
150 | 最短 |
shorter |
200 | — |
short |
250 | — |
standard |
300 | 最基础的推荐时长(create() 的默认值) |
complex |
375 | 复杂动画 |
enteringScreen |
225 | 元素进入屏幕 |
leavingScreen |
195 | 元素离开屏幕 |
主题上的 transitions.create(props, options) 会把这些拼成 prop duration easing delay 字符串;getAutoHeightDuration(height) 则按 4 + 15·h^0.25 + h/5 的公式为展开/收起动画动态计算时长(上限 3000ms)。
zIndex:全局层级表
zIndex.js 用注释强调"zIndex 像浏览器里的全局值,必须集中定义"。默认值:
| 层 | 值 |
|---|---|
mobileStepper |
1000 |
fab / speedDial |
1050 |
appBar |
1100 |
drawer |
1200 |
modal |
1300 |
snackbar |
1400 |
tooltip |
1500 |
各组件内部都从这里取值,保证 Snackbar 永远浮在 Modal 之上、Tooltip 永远最顶层的相对顺序。
motion 与 reducedMotion
createMotion.js 只有几行:默认返回 { reducedMotion: 'never' } 再合并用户输入。注意一个容易混淆的细节——createThemeNoVars.js 中有一行 delete muiTheme.transitions.reducedMotion(第 120 行),因为新版主题把 reducedMotion 的归属从 transitions 迁到了 motion,需要清掉 system 层遗留的旧值。
typography 与 mixins
typography 由 createTypography(palette, typographyInput) 基于调色板生成(字体族、字号、行高等),typography.md 同样提供"主题查看器 + window.theme.typography 控制台"两种探索方式;mixins 由 createMixins(breakpoints) 生成,默认包含 toolbar 等高/边距工具类,且依赖 @mui/system 层提供的 breakpoints(各断点文档 breakpoints.md 也是通过本页的 ?expand-path=$.breakpoints 链接来展示默认断点值的)。
颜色操作工具与 sx
拼装完成后,createThemeNoVars.js 还挂了几个运行时工具:
theme.alpha(color, coefficient)/theme.lighten()/theme.darken():当主题开启了colorSpace时,会输出oklch()或color-mix()表达式;开启 CSS 变量时则把var(--xxx)替换为var(--xxxChannel)以正确计算 alpha(见 attachColorManipulators);theme.unstable_sx(props):主题对象上的sx()快捷函数,内部调用@mui/system的styleFunctionSx;theme.toRuntimeSource:即stringifyTheme,源码注释标明是"for Pigment CSS integration",即主题对象可被序列化回运行时 CSS 源码;- 开发环境下(
NODE_ENV !== 'production')还有一段守护逻辑:如果用户在components.*.styleOverrides里直接写focused、disabled等内部状态类名,会console.error提示改用&.Mui-focused语法,并把该样式清空以防全局污染。
内置默认主题常量:defaultTheme.js
仓库里还有一个最小入口 defaultTheme.js:
import createTheme from './createTheme';
const defaultTheme = createTheme();
export default defaultTheme;
它调用的正是本文拆解的零参 createTheme(),导出的就是"纯净默认主题"对象,供包内组件(如未包裹 ThemeProvider 时)兜底使用。
默认主题查看器的实现:DefaultTheme.js
文档页的树状 demo(DefaultTheme.js)本身就值得细读,它展示了"如何在页面里交互式探索一棵主题对象"的完整套路。
1. 实时生成、剔除内部字段
const data = React.useMemo(() => {
const themeData = createTheme({
palette: { mode: darkTheme ? 'dark' : 'light' },
});
const { unstable_sxConfig: unstableSxConfig, unstable_sx: unstableSx, ...rest } = themeData;
return rest;
}, [darkTheme]);
组件用 useMemo 调用 createTheme() 现场生成主题树,并把 unstable_sxConfig、unstable_sx 两个非序列化友好的内部字段解构剔除,只把纯数据 rest 交给树形组件渲染。深色开关切换 palette.mode 后,palette、typography 中依赖调色板的字段会随之重新计算——这正是"看默认值"最直观的方式。
2. 通过 URL 参数 expand-path 深度展开
其他文档页(palette / typography / breakpoints)都是链接到本页并带 ?expand-path=$.palette 之类的查询参数。demo 在 useEffect 里解析它:
decodeURI(document.location.search.slice(1))
.split('&')
.forEach((param) => {
const [name, value] = param.split('=');
if (name === 'expand-path') {
expandPath = value;
}
});
拿到 $.palette 后,去掉 $. 前缀、按 . 拆段并累加还原成路径数组 ['palette'](嵌套写法如 $.typography.fontFamily 会还原为 ['typography', 'typography.fontFamily']),传给 ThemeViewer 的 expandPaths。于是从 palette 文档页点过来时,树自动展开到调色板分支——这就是文档体系里"主题查看器"作为共享锚点的机制。
3. 全展开开关与两个预构建视图
顶栏两个 StyledSwitch 分别控制:
- Expand all:不直接重渲染整棵树,而是在 第 125~133 行 用
useMemo预构建了两份ThemeViewer(一份expandPaths={[]}折叠态、一份expandPaths={allNodeIds}全展开态),切换开关只改 CSSdisplay,避免展开几百个节点时的卡顿; - Dark mode:即前文
createTheme({ palette: { mode } })的驱动源。
allNodeIds 来自 useItemIdsLazy(data)(ThemeViewer 的懒遍历工具),会递归收集树的每个节点 id。
4. 与文档站主题的区分
页面顶部还有一个文档站全局样式的小细节:demo 中 Switch 的暗色适配选择器写成 [`*:where(${theme.vars ? '[data-mui-color-scheme="dark"]' : '.mode-dark'}) &`],兼容"开启 CSS 变量时按 [data-mui-color-scheme] 属性、否则按 .mode-dark 类名"两种主题方案。也正因为文档站启用了自定义品牌主题,文档才特别提醒:页面上 window.theme 不是纯默认值——要看"出厂设置",请依赖本文展示的树视图(它现场 createTheme() 生成,不受站点主题影响)或本地 node/控制台执行 createTheme()。
在控制台探索默认主题:三种方式
结合文档与源码,实操时可按需选择:
- 文档页树视图:访问
/material-ui/customization/default-theme/,用开关切换深色模式、展开全树; - 控制台变量:文档任意页面执行
theme.palette、theme.transitions.create('width')等——但记住这是 MUI 品牌定制主题,palette.primary等已被覆盖; - 纯净默认值:本地执行
createTheme()(来自@mui/material/styles),或直接阅读上文列出的各create*工厂函数源码——默认值全部硬编码在这些文件里,无运行时魔法。
小结
- 默认主题由 createTheme.ts 分派,默认路径 createThemeNoVars.js 以"System 基础层 + Material 模块层"深度合并的方式组装;
- 顶层默认值均有明确的源码出处:25 级
shadows(shadows.js)、7 档zIndex(zIndex.js)、4 组easing+ 7 档duration(createTransitions.js)、motion.reducedMotion: 'never'(createMotion.js); - 新版主题把单 palette 模型升级为
colorSchemes+defaultColorScheme多方案模型,并为 CSS 变量场景(cssVariables: true)预留了 createThemeWithVars.js 分支; - 文档站的 Default Theme Viewer 通过现场调用
createTheme()、剔除unstable_*字段、支持?expand-path=$.xxx深链和双视图预渲染,成为整套 customization 文档共享的"默认值锚点"; - 相关行为均有测试覆盖,例如 createTheme.test.js 与 createTheme.spec.ts,可作为默认值断言的权威参照。
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