Material UI 样式定制指南:按层级选择 `sx`、`styled()`、主题覆盖与全局 CSS
Material UI(本仓库为 mui/material-ui)为开发者提供了从「一次性微调」到「全局基线」的四层样式定制策略。本文以仓库内置的 skills/material-ui-styling/AGENTS.md 为骨架,结合 How to customize、Themed components、The sx prop、styled() 等官方文档与对应可运行 Demo 源码,系统梳理每一种策略的适用场景、API 细节、状态类与槽位(slot)命名规则。读完你将能够快速判断一个改动该落在哪一层,并写出既符合设计体系、又避免样式「泄漏」到全局的 Material UI 代码。
版本提示:本指南对应的 Material UI 版本为 v9(
>=9.0.0 <10.0.0)。若你使用其他主版本,请以实际仓库与所安装包的 API 为准。
1. 概览:四种策略,按作用域从小到大选择
Material UI 的样式定制策略可以按作用域从窄到宽排成一条决策链:
- 一次性定制 / 局部布局 → 使用
sxprop - 同一套覆盖在多处复用 → 用
styled()包裹 MUI 组件(或做一个薄封装组件) - 某个组件所有实例的默认外观都要改变 → 使用
theme.components(styleOverrides、variants、defaultProps) - 原生 HTML 元素基线(例如所有
h1)或与单个组件无关的全局样式 → 使用GlobalStyles或CssBaseline覆盖
两条反向的告诫同样重要:不要为了一个一次性页面就跳到全局主题覆盖;反过来,如果是一个被大规模重复使用的系统样式,也不要只用 sx 到处粘贴——此时具名变体或 styled() 封装更清晰、更易维护。这也是 AGENTS.md 中强调「选择能解决问题的最小作用域,以避免全局规则被四处打散」的原因。
2. 一次性定制:sx prop
2.1 使用场景与能力
当只需要修改单个实例(或很小的内联场景),且能访问到主题时,首选 sx。仓库中的基础示例 SxProp.js 展示了它的典型形态:
import Slider from '@mui/material/Slider';
export default function SxProp() {
return <Slider defaultValue={30} sx={{ width: 300, color: 'success.main' }} />;
}
color: 'success.main' 这类值并不是合法 CSS,它是 sx 提供的主题感知属性:color 会解析为 theme.palette.success.main 路径,width: 300 会变成 300px。MUI System 把所有样式函数打包进了 sx 这个「CSS 超集」里,因此你既能写任意标准 CSS,也能用主题快捷值。官方 The sx prop 中罗列了以下典型映射:
- 边框:
border: 1等价于border: '1px solid black';borderColor: 'primary.main'等价于取theme.palette.primary.main;borderRadius: 2表示2 * theme.shape.borderRadius(默认每个单位4px)。 - 间距:
margin/padding及其长写属性会把数值乘以theme.spacing(默认每个单位8px),并支持大量别名——m、mt、mr、mb、ml、mx、my、p、pt、pr、pb、pl、px、py。 - 调色板:
color、bgcolor(backgroundColor别名)接受主题调色板路径字符串。 - 网格:
gap、rowGap、columnGap的数值会乘以theme.spacing。 - 定位 / 阴影:
zIndex: 'tooltip'映射到theme.zIndex.tooltip;boxShadow: 1映射到theme.shadows[1]。 - 字号排版:
fontWeight: 'fontWeightLight'(或省略前缀的'light')映射到theme.typography;typography: 'body1'会展开theme.typography.body1的全部值。 - 尺寸:
width: 1/2会转换为'50%'(值在(0, 1]之间时按百分比处理),width: 20则输出'20px'。
此外 sx 还支持伪选择器、嵌套选择器、响应式对象(断点与容器查询)、回调与数组值。
2.2 数组语法:条件合并的正确姿势
当需要根据条件叠加样式时,用数组形式代替对象展开(spread):
<Box
sx={[
{
'&:hover': {
color: 'red',
backgroundColor: 'white',
},
},
foo && {
'&:hover': { backgroundColor: 'grey' },
},
bar && {
'&:hover': { backgroundColor: 'yellow' },
},
]}
/>
规则是:数组按顺序应用,falsy 条目会被跳过;同一属性上索引越大优先级越高——因此上例中即使 foo 为真,只要 bar 为真,backgroundColor 就是 yellow。每个索引既可以是一个样式对象,也可以是一个接收 theme 的回调:
sx={[
{ mr: 2, color: 'red' },
(theme) => ({
'&:hover': {
color: theme.palette.primary.main,
},
}),
]}
注意:回调应作为整个 sx 值使用(sx={(theme) => ({...})}),属性值位置的回调形式已废弃。TypeScript 下把样式对象单独声明时需要 as const,以避免字面量类型被拓宽为 string 导致类型不匹配。
2.3 覆盖组件内部槽位(slots)
要修改组件的某个内部子结构,可以在 sx 里用全局类名片段定位槽位,例如把 Slider 的滑块 thumb 改成方形:
import Slider from '@mui/material/Slider';
export default function DevTools() {
return (
<Slider
defaultValue={30}
sx={{
width: 300,
color: 'success.main',
'& .MuiSlider-thumb': {
borderRadius: '1px',
},
}}
/>
);
}
完整 Demo 见 DevTools.js。这里的思路是:先在浏览器 DevTools 里找到目标槽位的类名。Material UI 注入的类名遵循固定模式 [hash]-Mui[组件名]-[槽位名],例如 .css-ae2u5c-MuiSlider-thumb——但 hash 部分不稳定,绝不能拿来写选择器,只能依赖稳定的 Mui[Component]-[slot] 片段(如 .MuiSlider-thumb)。该命名规范与更完整的槽位说明参见 reference.md。
2.4 状态类(state classes)与特异性
hover、focus、disabled、selected 这类状态在 Material UI 里用高特异性样式表达。能使用原生伪类的状态(如 :disabled)优先使用伪类;但像 selected 这类 Web 规范中不存在对应伪类的状态,Material UI 提供了状态类(全局类名),其特异性与 CSS 伪类相当。覆盖它们时必须提高特异性,永远不要把状态类当裸全局选择器使用:
/* ❌ 错误:会污染所有使用 .Mui-error 的组件 */
.Mui-error {
color: red;
}
/* ✅ 正确:限定到 OutlinedInput 的根元素上 */
.MuiOutlinedInput-root.Mui-error {
color: red;
}
对完整状态类清单与规则,见 How to customize 的 "State classes" 小节及下方的状态类速查表。
2.5 className:接入外部 CSS / CSS Modules
当需要与外部样式库或 CSS Modules 协作时,使用组件自带的 className prop,并按同样的「槽位 + 状态类」规则组合选择器(例如 className="Button" 配合 .Button:disabled 提权)。各样式库的完整接入方式见仓库的 interoperability 相关示例。
3. 可复用:styled()
3.1 何时使用
当同一个自定义组件要出现在多个位置、且值得拥有一个具名组件时,把 styled() 作为首选:
import { styled } from '@mui/material/styles';
使用 Material UI 时应优先从 @mui/material/styles 导入,这样在应用没有通过 ThemeProvider 提供主题时,会用与其余应用一致的默认主题兜底;而 @mui/system 版本的 styled 使用另一套默认主题。仓库中的可运行对照例子是 StyledCustomization.js:
import Slider from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';
const SuccessSlider = styled(Slider)(({ theme }) => ({
width: 300,
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
'&.Mui-active': {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
}));
export default function StyledCustomization() {
return <SuccessSlider defaultValue={30} />;
}
相比底层样式库(Emotion / styled-components)的 styled(),Material UI 版本额外提供了四个能力(见 styled() 文档):
- 无主题上下文时使用默认主题;
- 支持通过
options.name关联theme.components[name]下的styleOverrides与variants; - 生成的组件自带
sxprop(可用options.skipSx关闭); - 默认内置
shouldForwardProp处理,避免把ownerState、theme、sx、as等透传到 DOM。
3.2 options 参数速览
styled(Component, options)(styles) 中可用的关键 options:
shouldForwardProp:(prop: string) => bool,决定某个 prop 是否继续传递给底层组件;name:theme.components下用于查找styleOverrides/variants的键,同时也用于生成调试用 label;slot:通常为'Root',为Root时自动应用主题里该组件的variants;overridesResolver:自定义如何根据 props 解析theme.components[name].styleOverrides;skipVariantsResolver:设为true可关闭对theme.components[name].variants的自动解析;skipSx:设为true可关闭结果组件上的sx支持。
3.3 自定义 props 与动态样式
为自定义组件新增 props 时,务必使用 shouldForwardProp 把非 DOM 属性拦下来,避免 React 把无效属性渲染到 DOM 上;TypeScript 侧则通过继承组件 props 类型并做接口扩展。仓库 DynamicCSS.js 演示了标准写法:
const StyledSlider = styled(Slider, {
shouldForwardProp: (prop) => prop !== 'success',
})(({ theme }) => ({
width: 300,
variants: [
{
props: ({ success }) => success,
style: {
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
'&.Mui-active': {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
},
},
],
}));
动态样式的两条实现路径建议:
- 首选 CSS 变量或样式回调中的条件样式对象:把「按 props 分支」收敛在一个顶层函数里,可读性最好;不要在样式对象内部给每个字段单独写
(props) => ...形式的函数。 - 需要随高频变化值(如颜色选择器实时预览)改样式时,优先用内联 CSS 变量而不是每帧传入新对象,避免不断向 DOM 插入无谓的
<style>标签而引发性能问题(参见 The sx prop—Dynamic values)。
4. 全站一致:createTheme({ components })
4.1 何时使用
当希望 Button、TextField 等组件默认外观在全应用范围内统一变化时,使用主题的 components 键。它的三个子键分工明确:defaultProps 改默认 props、styleOverrides 改槽位样式、variants 按 props 映射附加样式。完整的组件键与槽位名需要查阅对应组件的 "Customization" 文档小节,键名与组件内部名一致(如 MuiButton、MuiTextField)。
关键告诫:主题是不可 tree-shaking 的(见 theme-components.md)。如果某个定制很重却只用在一两处,新建一个组件通常比把主题越撑越大更好。
4.2 defaultProps:修改默认 props
const theme = createTheme({
components: {
// 组件名
MuiButtonBase: {
defaultProps: {
// 要修改默认值的 props
disableRipple: true, // 全应用关闭涟漪效果
},
},
},
});
4.3 styleOverrides:按槽位覆盖样式
styleOverrides 以槽位名为键(root 表示最外层元素),值为 CSS-in-JS 样式对象,也支持嵌套选择器:
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
// 槽位名
root: {
// CSS 属性
fontSize: '1rem',
},
},
},
},
});
当需要依据组件已解析的 props(ownerState) 或主题值分支时,可把某槽位值写成回调形式(例如 root: ({ ownerState, theme }) => ({ ... }))。需要注意:仓库内 Themed components 文档指出,以回调访问槽位 ownerState 的形式已被标记为废弃(deprecated),官方建议改用 variants 表达按 props 分支的样式:
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
- root: ({ ownerState, theme }) => ({ ... }),
+ root: {
+ variants: [...],
},
},
},
},
});
4.4 variants:把 props 映射成样式
variants 定义在具体槽位下,是 { props, style } 对象构成的数组。当组件 props 与 props 匹配时应用 style;需要优先的样式要放在数组后面。
针对既有 props 增加样式(例如加粗 outlined 类型的 Card 边框):
const theme = createTheme({
components: {
MuiCard: {
styleOverrides: {
root: {
variants: [
{
props: { variant: 'outlined' },
style: {
borderWidth: '3px',
},
},
],
},
},
},
},
});
为 Button 增加全新变体 dashed(取值名可自定义):
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
root: {
variants: [
{
props: { variant: 'dashed' },
style: {
textTransform: 'none',
border: `2px dashed ${blue[500]}`,
},
},
],
},
},
},
},
});
也可以同时匹配既有与新增 props,甚至把 props 写成回调来处理「属性不等于某值」这类条件(例如 variant === 'dashed' && color !== 'secondary')。若在 TypeScript 中使用新增变体,需要通过[模块增强]声明 ButtonPropsVariantOverrides:
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
dashed: true;
}
}
仓库还用类型测试锁定了这一用法(见 packages/mui-material/test/typescript/augmentation/ 下的组件主题相关 spec)。
4.5 在主题里使用 sx 语法(实验性)
sx 在组件上自 v5 起就是稳定特性,但在主题对象内部直接使用仍属实验性。若你已经在用 sx,可以用同样的语法快速迁移样式到主题:
const finalTheme = createTheme({
components: {
MuiChip: {
styleOverrides: {
root: ({ theme }) =>
theme.unstable_sx({
px: 1,
py: 0.25,
borderRadius: 1,
}),
label: {
padding: 'initial',
},
icon: ({ theme }) =>
theme.unstable_sx({
mr: 0.5,
ml: '-2px',
}),
},
},
},
});
由于 sx 的 CSS 特异性高于主题覆盖,即便在主题中用了这套语法,仍可继续在组件上通过 sx 覆盖它。
5. 全局 CSS:GlobalStyles 与 CssBaseline
5.1 何时使用
当要样式化原生 HTML 元素(例如所有 h1)或编写不绑定到某个 MUI 组件实例的全应用级代码片段时使用。
5.2 GlobalStyles 基本用法
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function GlobalCssOverride() {
return (
<React.Fragment>
<GlobalStyles styles={{ h1: { color: 'grey' } }} />
<h1>Grey h1 element</h1>
</React.Fragment>
);
}
完整示例见 GlobalCssOverride.js。当需要访问主题时,把 styles 写成回调即可:
<GlobalStyles
styles={(theme) => ({
h1: { color: theme.palette.primary.main },
})}
/>
(见 GlobalCssOverrideTheme.js。)
性能建议:把 <GlobalStyles /> 提升为模块级常量再复用,避免每次渲染都重新计算 <style> 标签、造成重复的样式计算:
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
+const inputGlobalStyles = <GlobalStyles styles={...} />;
function Input(props) {
return (
<React.Fragment>
- <GlobalStyles styles={...} />
+ {inputGlobalStyles}
<input {...props} />
</React.Fragment>
);
}
5.3 通过 MuiCssBaseline 扩展全局基线
CssBaseline 用于为应用提供统一的 HTML 基线(去除 body 默认 margin、应用 theme.palette.background.default 背景、全局 border-box、滚动条与 color-scheme 等,详见 CSS Baseline)。如果你已经在用 CssBaseline,与其再叠加一个 GlobalStyles,不如直接扩展 MuiCssBaseline 的 styleOverrides:
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiCssBaseline: {
styleOverrides: `
h1 {
color: grey;
}
`,
},
},
});
export default function OverrideCssBaseline() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>Grey h1 element</h1>
</ThemeProvider>
);
}
MuiCssBaseline 的 styleOverrides 同样支持回调形式以访问主题(参考 OverrideCssBaseline.js 及配套的 callback 示例)。若只是渐进式迁移某块局部区域而不想全局重置,可以考虑 ScopedCssBaseline,把基线限定在其子节点内。
6. sx vs styled():Agent 与开发者都应知道的差异
两者经常被混淆,Material UI 官方文档与 AGENTS.md 都专门列出一张差异对照表:
| 主题 | sx |
styled() 样式对象 |
|---|---|---|
主题间距简写(m、p、gap 等) |
✅ 支持 | ❌ 不支持。需在回调中用 theme.spacing() 或写普通 CSS 值 |
数字 padding(如 1)的含义 |
主题间距单位(theme.spacing(1)) |
像素(1px),不是 theme.spacing(1) |
主题调色板字符串(如 'primary.main') |
✅ 支持 | 需在回调中使用 theme 取值 |
由此可总结出三条最容易踩的「坑」:
styled('button')({ mx: 1 })是错误用法——mx这类系统布局简写只在sx中可用,styled()的样式对象是纯 CSS,必须写margin-left: theme.spacing(1)。styled('button')({ padding: 1 })中的1代表1px;而sx={{ padding: 1 }}中的1代表theme.spacing(1)。语义完全不同。- 想在
styled()里复用一套sx逻辑时,可借助主题的unstable_sx(写法参考上文 4.5 节),细节见 styled()—Difference with the sx prop。
7. 导入与一致性约定
为了让代码整洁且利于打包,仓库内的 AGENTS.md 给出了两条应用层约定:
- 优先使用包的一级导入,例如
@mui/material/Button,避免通过桶文件(barrel)把整个@mui/material拉进产物; - 系统布局简写(
p、gap、mt等)用sx表达;行为相关的、由组件文档定义的 props 用组件 props 表达——不要把布局语义塞进variant之类的行为性 props 里。
示例 Demo 基本都遵循这一约定,例如 SxProp.js 从 @mui/material/Slider 单路径导入组件,而样式靠 sx 完成。
8. 附录:状态类与类名速查
以下状态类与命名规范在 AGENTS.md 的配套文件 reference.md 以及 How to customize 中均有完整记载,使用时务必与组件或限定作用域的选择器组合,绝不单独出现。
8.1 全局状态类
| 状态 | 全局类名 |
|---|---|
| active | .Mui-active |
| checked | .Mui-checked |
| completed | .Mui-completed |
| disabled | .Mui-disabled |
| error | .Mui-error |
| expanded | .Mui-expanded |
| focus visible | .Mui-focusVisible |
| focused | .Mui-focused |
| readOnly | .Mui-readOnly |
| required | .Mui-required |
| selected | .Mui-selected |
8.2 槽位类名模式
- 注入的类名遵循
[hash]-Mui[组件名]-[槽位名]。 - 写选择器时只用稳定片段
Mui[组件名]-[槽位名](如.MuiSlider-thumb),禁止使用含 hash 的完整类名,因为 hash 不稳定。
8.3 主题组件键
- 通过
createTheme({ components: { MuiButton: { ... } } })覆盖组件。 - 键名与组件内部名一致(例如
MuiButton、MuiTextField);具体槽位名和可用的主题键请查阅对应组件文档的 Customization 小节。
9. 进一步阅读
| 主题 | 仓库内文档 |
|---|---|
| 策略总览与决策 | How to customize |
sx 参考 |
The sx prop |
styled() API |
styled() |
| 组件级主题 | Themed components |
| CSS 基线 | CSS Baseline |
| 状态 / 类名速查 | reference.md |
在动手前可以再核对一遍 AGENTS.md 中的决策清单:先问「是单实例还是局部布局?」→ 用 sx;再问「同一覆盖是否多处复用?」→ 用 styled();接着问「是否所有实例默认外观都要变?」→ 用 theme.components;最后才考虑「是否是原生元素基线或非组件全局 CSS?」→ 用 GlobalStyles / CssBaseline。按此顺序,从最小作用域开始,你就能始终把定制放在最合适、最不易产生副作用的那一层。
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 StartedRust0627
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