Material UI v9 主题与设计令牌实战指南:从 createTheme 到 CSS 变量与 Windows 高对比模式
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
适用版本说明:本文所述 API 面向 Material UI v9(版本范围
>=9.0.0 <10.0.0)。该技能指南以 AGENTS.md 为纲,技术细节扎根于仓库docs/data/material/customization/(theming、palette、dark-mode、css-theme-variables、typography、spacing、shape)与packages/mui-material/src/styles/下的源码实现。若你正在使用其他大版本,请先核对本文 API 细节再行套用。
Material UI 的主题机制本质上是一份设计令牌对象:palette、typography、spacing、shape、breakpoints、zIndex、transitions 等,再加上可选的按组件默认值theme.components。要建立一套可切换明暗、可配合服务端渲染、甚至能适配 Windows 高对比模式的统一视觉体系,你需要掌握createTheme/ThemeProvider的基础用法、colorSchemes与cssVariables的取舍,以及自定义品牌令牌时对应的 TypeScript 类型增强方法。读完本文,你将能够在 Material UI v9 项目中独立搭建并扩展一套生产级主题,并理解其背后的源码原理。
一、主题的核心心智模型:一份对象 + 一次注入 + 三处读取
先用一句话概括 Material UI 主题化的工作方式:
一个 theme 就是一个包含设计令牌(design tokens)和可选组件级默认值的 JavaScript 对象;应用通常在启动阶段调用一次
createTheme(或分步组合),把结果通过靠近根节点的<ThemeProvider>注入 React context,组件再通过useTheme、sx或styled读取令牌值。
以createTheme为分水岭,可以把它拆成三层来理解:
- 令牌层:
palette、typography、spacing、shape、breakpoints、zIndex、transitions,它们描述"设计上应该长什么样"; - 行为层:
theme.components按Mui*组件键注入defaultProps、styleOverrides、variants,让某个组件在全局范围内有一致的默认外观; - 注入层:
ThemeProvider把 theme 对象放进 React context,任何子孙组件都能拿到。
这套模型的源码起点位于 createTheme.ts:从源码结构看,当cssVariables未开启(默认false)时,它内部走createThemeNoVars路径,其注释明确写着"Behaves exactly as v5";而当开启 CSS 变量或colorSchemes时,则会进入带 vars 的处理管线。这与下文"CSS 变量是可选项"的说法一致——不使用 CSS 变量时,你的主题行为完全等价于经典的 v5 语义。
二、核心起步:createTheme + ThemeProvider + CssBaseline
基础三段式代码如下:
import { createTheme, ThemeProvider } from '@mui/material/styles'; const theme = createTheme({ palette: { primary: { main: '#1976d2' }, }, }); function App() { return ( <ThemeProvider theme={theme}> {/* 应用内容 */} </ThemeProvider> ); }要点说明:
- 主题构建入口统一从
@mui/material/styles导入createTheme(产出 Material UI 默认主题语义)。 - 用
createTheme({ ... })构建主题对象后,用<ThemeProvider theme={theme}>包裹应用,使所有后代组件都能通过 context 拿到主题。 - 在 Provider 内部放置
<CssBaseline />,以获得基准元素样式与正确的暗色背景行为(细节见 Dark mode)。
组件内读取主题的官方入口同样是@mui/material/styles下的useTheme():
import { useTheme } from '@mui/material/styles'; function MyComponent() { const theme = useTheme(); return <div style={{ color: theme.palette.primary.main }} />; }除useTheme之外,更推荐的方式是通过sx或styled的插值函数取令牌,例如sx={{ color: 'primary.main' }}与styled('div')(({ theme }) => ({ color: theme.palette.primary.main }))——它们会自动把语义色名解析为主题值。
三、设计令牌全景图(Design Token Map)
下表来自该指南的核心表格,罗列了令牌存放的各个区域及其角色:
| 区域 | 作用 | 参考文档 |
|---|---|---|
palette | 语义色(primary、secondary、error…)、文字、背景、分割线、action 色 | docs/data/material/customization/palette/ |
typography | fontFamily、fontSize、字体变体(h1~body2、button…) | docs/data/material/customization/typography/ |
spacing | theme.spacing(n)间距标尺(默认每单位 8px) | docs/data/material/customization/spacing/ |
shape | borderRadius(默认 4);新增额外圆角需做 TypeScript 类型增强 | docs/data/material/customization/shape/ |
breakpoints | 供sx/ 媒体查询使用的响应式键 | docs/data/material/customization/breakpoints/ |
zIndex | 层级令牌 | docs/data/material/customization/z-index/ |
transitions | 时长 / 缓动函数辅助 | docs/data/material/customization/transitions/ |
components | 按Mui*键配置defaultProps、styleOverrides、variants | docs/data/material/customization/theme-components/ |
完整的默认值明细可参考仓库中docs/data/material/customization/default-theme/下的默认主题文档(Default theme explorer),它逐项列出每个令牌在生产主题里的默认值,适合作为调试"这个值为什么是这个颜色"的对照表。
从源码结构看,令牌在生产主题中并非全部原样暴露:例如 createTheme.ts 会通过createMixins、createTypography、createPalette等工厂函数把简写输入(如只给palette.primary.main)展开成包含light/dark/contrastText等派生字段的完整结构,这也是"只给main也能跑"的底层原因。
四、Palette 速查:三个高频事实
使用 palette 时应记住以下三点:
- 每个 palette 颜色通常包含
main、light、dark、contrastText四个字段。多数情况下只需提供main,createTheme会根据亮度算法自动推导其余字段与对比度文字色。这条规则同样适用于primary、secondary这类语义色键之外的自定义键(status.danger之类)。 - 构建调色板时优先复用
@mui/material/colors,例如import { purple } from '@mui/material/colors'后取purple[500],以获得符合 Material Design 规范的现成色阶,而不是手写难以对齐的十六进制色。 palette.mode: 'dark'会把整套主题强制切换成暗色调色板。注意:如果使用全自定义调色板并配合暗色模式,务必保证自定义色值与当前 mode 的语义相符——例如暗色下背景应使用深色系,否则可能出现深底浅字之外的错乱观感。详见 Dark mode。
五、colorSchemes vs palette-only 暗色:系统级明暗方案的正确姿势
如果你需要"跟随系统偏好、跨标签页同步、切换时可关闭过渡动画、兼容 SSR"这类完整的明暗切换体验,指南明确建议使用colorSchemes而非旧的、能力较窄的"仅设palette.mode"方案。
import { createTheme } from '@mui/material/styles'; // 内置 light + dark 两套 scheme const theme = createTheme({ colorSchemes: { dark: true }, });几个必须牢记的判定规则:
colorSchemes与palette同时存在时,palette优先。避免无意中覆盖。这一点在源码中有直接体现:createTheme 内部会把传入的palette通过attachColorScheme逻辑合并进对应 scheme(createTheme.ts 附近的attachColorScheme会以palette为准重建 scheme 的palette)。defaultColorScheme默认取palette?.mode:从 createTheme.ts 可见,未显式声明palette时默认colorSchemes = { light: true },默认 scheme 在未指定palette.mode时回落到'light'。也就是说,palette.mode: 'dark'依然可以表达"默认就是暗色",只是它不再被当作独立的运行时切换机制。- 用
useColorScheme读取 / 更新当前 mode 以实现切换。注意首次渲染时mode可能是undefined(系统偏好尚未确定 / SSR 尚未水合),代码需显式处理该分支,避免水合(hydration)不一致。 ThemeProvider上支持storageManager、disableTransitionOnChange、noSsr等属性,用于定制持久化存储、切换 scheme 时关闭过渡动画,以及控制 SSR 期间的颜色方案行为。这些细节都收录在 Dark mode 文档中。
如果你已经用 v5 时代的"静态palette.mode+ 两个 theme 手动切换"方案工作了很久,迁移到colorSchemes的核心收益在于:系统偏好监听、多标签页同步、以及 scheme 切换时避免整页重挂载的体验,都可以交给框架层处理。
六、开启 CSS 主题变量:cssVariables: true 与 theme.vars
当需要"更清晰的调试、暗色局部区域的更少主题嵌套、以及切换时更少的 JS 计算"时,可以在createTheme中开启cssVariables: true:
import { createTheme } from '@mui/material/styles'; const theme = createTheme({ cssVariables: true, colorSchemes: { light: true, dark: true }, });开启后,组件的样式将使用var(--mui-...)形式的 CSS 变量;对应地,在样式回调中应优先使用theme.vars(它把 palette / typography 等令牌镜像成指向 CSS 变量的引用),而不是直接读theme.palette.*的静态值。用法细节见 Usage。
关于cssVariables的选项形态,从 createTheme.ts 的类型定义可以看出它既可以传boolean,也可以传Pick<CssVarsThemeOptions, CssVarsConfigList>对象做更细粒度的配置(例如通过colorSchemeSelector控制选择器形态)。
四个高频注意事项:
- 不要向
createTheme传自定义的vars键。该键为 CSS 变量功能保留,由框架自动生成,手动传入会与自动生成逻辑冲突。 - CSS 变量下的暗色专属样式,用
theme.applyStyles('dark', { ... }),而不要用基于theme.palette.mode的分支写法——后一种方式在方案切换时容易造成闪烁。官方在 Usage 与 Configuration 中均有明确警告。 - 防首帧闪烁脚本的位置:
InitColorSchemeScript必须放在任何渲染内容之前,以阻止初始 color-scheme 的闪烁。具体到路由框架:- App Router:放在
app/layout.tsx的<body>内、{children}之前; - Pages Router:放在
_document.tsx中、<Main />之前。
- App Router:放在
- 权衡取舍:开启后 HTML 体积会变大(同时输出明暗两套方案的变量,可能影响 FCP);收益是切换 scheme 时更少的 JS 工作量与更好的 SSR 暗色体验。综述见 Overview。
此外,老版本的CssVarsProvider已被具备同等能力的ThemeProvider取代——v9 中统一使用ThemeProvider(配合cssVariables与colorSchemes选项)即可。
关于theme.vars的类型:theme.vars的类型默认并未启用。若要在 TypeScript 中使用,需要参照 Usage 中的 TypeScript 章节开启相关类型声明(见 reference.md)。当组件可能运行在ThemeProvider之外时,推荐使用兼容两种形态的兜底写法:
backgroundColor: (theme.vars || theme).palette.primary.main;这段代码同时覆盖"开启了 cssVariables(有theme.vars)"与"未开启(只有theme.palette)"两种运行时,是 reference.md 提供的官方推荐 fallback 写法。
七、Typography 与 Spacing:两个最容易踩坑的度量体系
排版
- Material UI 的排版使用
rem单位,默认根字号语义与设计规范见 Typography 文档。 - 可通过调整
typography.fontSize(修改基准字号)或逐个字体变体(h1~body2、button等)的fontSize来覆盖。 - 若希望排版随断点整体缩放,用
responsiveFontSizes(theme)包装一次即可(它位于@mui/material/styles,与enhanceHighContrast属于同一类"主题增强器"模式):
import { createTheme, responsiveFontSizes } from '@mui/material/styles'; let theme = createTheme(); theme = responsiveFontSizes(theme);间距
theme.spacing(n)遵循配置好的间距标尺,默认每单位 8px(spacing(2)即 16px);sx里的间距简写(如p: 2、gap: 1)走同一套系统,因此sx与theme.spacing语义天然一致。- 数组形式的
spacing配置存在表达力限制(对负数、小数、'auto'支持不完整)。需要完整表达力时,在主题中把spacing配成函数形式,而不是数组。
八、主题的组合与合并:分步 createTheme 与 deepmerge
现实项目中经常出现"某个令牌要由另一个令牌推导而来"的需求。官方推荐的分步构建法:
import { createTheme } from '@mui/material/styles'; // 第一步:用基础选项产出完整主题 const baseTheme = createTheme({ palette: { primary: { main: '#1976d2' } }, }); // 第二步:把第一步的成果当作输入,再派生新主题 const derivedTheme = createTheme(baseTheme, { typography: { h1: { color: baseTheme.palette.primary.main }, }, });这等价于官方文档 "Using theme options to define other options"(主题组合)章节的推荐实践。要点在 Theming 文档 中亦有展开。
两条硬性规则:
- 不要把多个参数当作"自动深合并"来依赖——
createTheme只会正式处理第一个参数(对应官方 "createTheme(options, ...args)" 的说明)。也就是说,createTheme(a, b)里的b不会被规范地深合并进结果。 - 自行完成深合并后再一次性传入。可以借助
@mui/utils的deepmerge:
import { deepmerge } from '@mui/utils'; import { createTheme } from '@mui/material/styles'; const theme = createTheme(deepmerge(baseOptions, partialOverrides));这种"显式深合并 + 单对象传入"的写法能保证前向兼容,也不会对参数处理次序产生隐式依赖。
九、嵌套 ThemeProvider:局部覆盖外层主题
ThemeProvider支持嵌套,内层 Provider 覆盖外层:
import { createTheme, ThemeProvider } from '@mui/material/styles'; const outer = createTheme({ palette: { primary: { main: '#1976d2' } } }); const inner = createTheme({ palette: { primary: { main: '#9c27b0' } } }); <ThemeProvider theme={outer}> <App /> <ThemeProvider theme={inner}> <IsolatedSection /> {/* 此处用紫色 primary */} </ThemeProvider> </ThemeProvider>只有当你有意在父主题基础上扩展时,才使用函数式写法theme={(outerTheme) => createTheme({ ...outerTheme, ...overrides })}——此时outerTheme是已完成解析的完整主题对象,{ ...outerTheme }会把外层全部令牌带进新主题。注意函数式写法要求拿到的是"已解析主题"而非"原始 options",所以它适合在 Provider 层级做基于父主题的增量定制,而不是用来替代createTheme的一次性组合。
十、自定义品牌设计令牌与 TypeScript 类型增强
当你需要承载品牌专属的设计键时(例如status.danger),分两步走:
第 1 步:在createTheme中挂载自定义键:
import { createTheme } from '@mui/material/styles'; const theme = createTheme({ status: { danger: '#e53e3e' }, palette: { primary: { main: '#1976d2' }, // 需要新增调色板字段时,参照 palette 文档的增强模式 }, });第 2 步:同时增强Theme与ThemeOptions两个接口(完整模板见 reference.md):
declare module '@mui/material/styles' { interface Theme { status: { danger: string }; } interface ThemeOptions { status?: { danger?: string }; } }必须同时增强两者,是因为Theme描述的是"已解析主题"的形态,而ThemeOptions描述的是createTheme入参的形态——只增强其一,要么是入参时类型报错,要么是消费时读不到类型。
三条相关的扩展守则:
- 扩展
shape:新增圆角键时必须同时增强Shape与ShapeOptions两个接口(参考 reference.md 中的说明),这是shape类型体系的特殊要求。 - 扩展 palette:往 palette 加业务字段时,遵循 palette 文档中的 TypeScript 增强模式,同样要保证
Palette/PaletteOptions两侧类型一致。 - 严禁把
theme.vars用作自定义属性名:它是 CSS 变量支持功能的私有字段(AGENTS.md 与 reference.md 均明确警示),占用它会与框架自动生成的 vars 结构冲突。
十一、Windows 高对比模式:enhanceHighContrast 主题增强器
enhanceHighContrast是 Material UI v9 提供的一个主题增强器(与responsiveFontSizes属于同一设计模式),作用是为 MUI 组件追加@media (forced-colors: active)覆盖,从而在 Windows 高对比 / 强制颜色(Forced Colors)模式下保持良好的可读性。
最小用法:
import { createTheme, enhanceHighContrast } from '@mui/material/styles'; const theme = enhanceHighContrast(createTheme());三个关键事实(均有源码佐证,实现见 enhanceHighContrast.ts):
- 它接收一个已完全创建的 theme,返回增强后的副本——务必在
createTheme之后调用,绝不能包在createTheme内部。 - 默认使用 CSS 系统颜色关键字(
Highlight、HighlightText、ButtonBorder等),而非项目色板中的具体色值。默认 token 全集如下表(摘自 enhanceHighContrast.ts 的defaultHcTokens):
| Token | 默认系统色关键字 | 用途 |
|---|---|---|
disabled | GrayText | 禁用元素颜色 |
error | ActiveText | 错误状态色 |
selectedBackground | SelectedItem | 选中项背景 |
selectedText | SelectedItemText | 选中项文字 |
activeBackground | Highlight | 激活/选中(toggled)控件背景 |
activeText | HighlightText | 激活/选中控件文字 |
buttonBorder | ButtonBorder | 交互控件边框 |
buttonText | ButtonText | 按钮文字/图标 |
canvas | Canvas | 页面/画布背景 |
- 需要对齐品牌色时可传第二个参数覆盖个别 token:
const theme = enhanceHighContrast(createTheme(), { activeBackground: 'SelectedItem', // 例:切换/激活控件背景 activeText: 'SelectedItemText', });约束:token 值只能传 CSS 系统颜色关键字——因为只有这些关键字是浏览器能保证与配对的 token 稳定保持对比度的值(CSS Color 4 规范定义的 system colors)。传入项目十六进制色既可能破坏高对比语义,也可能因用户系统主题不同而失效。
从源码还可以看到两个值得注意的实现细节(enhanceHighContrast.ts):
- 函数内部把所有 HCM 覆盖合并进
theme.components的styleOverrides,覆盖组件涵盖MuiAccordionSummary、MuiAutocomplete、MuiCheckbox、MuiFilledInput、MuiFormControlLabel、MuiFormHelperText、MuiFormLabel、MuiInput、MuiLinearProgress、MuiInputBase、MuiMenuItem、MuiListItemIcon、MuiListItemButton、MuiNativeSelect、MuiOutlinedInput、MuiRadio、MuiSlider、MuiSwitch、MuiButtonBase、MuiTooltip、MuiToggleButton等常用表单与导航组件; - 它刻意用数组形式合并
styleOverrides,使每个条目作为独立 CSS 规则输出,让浏览器级联(而非 JS 对象合并)来裁决优先级——这对forced-colors覆盖与原主题样式共存是必要的。
由于对MuiSlider、MuiSwitch这类"track / thumb 子元素拿不到 disabled class"的组件,源码改用ownerState回调判断禁用态(enhanceHighContrast.ts),这也提醒你:当自定义 styleOverrides 需要感知禁用态时,优先依赖ownerState而不是 DOM class。相关测试见 enhanceHighContrast.test.ts,可当作验证函数行为与回归边界的参考。
十二、进一步阅读:仓库内一手资料索引
| 主题 | 仓库内一手资料 |
|---|---|
| Theming 概览与 API(组合、嵌套、自定义变量) | theming.md |
| 暗色模式与切换(colorSchemes、storage、SSR) | dark-mode.md |
| CSS 主题变量综述 | overview.md |
| CSS 主题变量用法(theme.vars、applyStyles) | usage.md |
| CSS 主题变量配置(InitColorSchemeScript、SSR 防闪烁) | configuration.md |
| Windows 高对比模式 token 全集与示例 | docs/data/material/customization/palette/(Palette 文档 High Contrast 章节) |
| 色板 / 品牌色阶工具 | docs/data/material/customization/color/ |
| TypeScript 主题定制(Theme / ThemeOptions 增强) | theming.md 与 reference.md |
最后强调一遍最容易犯的三个错误,对照自查:一是把palette与colorSchemes混用导致误覆盖(前者优先);二是直接读写保留字段vars;三是在 cssVariables 场景下用theme.palette.mode分支编写暗色样式而非theme.applyStyles。避开这三点,你的主题体系就能同时稳定服务于明暗切换、SSR 渲染与无障碍高对比场景。
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考