Material UI 的 `styled()` 指南:从基础用法到源码级原理详解
导读
styled() 是 MUI System 提供的组件样式化工具,也是 Material UI 中所有核心组件的样式实现底座。它构建于 @mui/styled-engine(内部默认封装 emotion)之上,在保留底层 styled 全部能力的同时,额外带来默认主题兜底、theme.components 主题化(styleOverrides / variants)、sx 属性支持与默认 prop 过滤等特性。读完本文,你将掌握 styled(Component, [options])(styles) 的完整 API、如何在自定义组件中复刻核心组件的主题能力、如何裁剪不需要的特性、如何创建自定义 styled(),以及它与 sx 属性在使用语法上的本质差异。
styled() 是什么,它解决什么问题
按官方文档 styled.md 的定义,styled() 是一个「用于创建样式化组件的工具」(Utility for creating styled components)。Material UI 的每一个核心组件都是基于它构建的——这意味着理解它,就等于理解了 Material UI 组件样式化的底层范式。
它直接对标的底层工具是 emotion 或 styled-components 的 styled()。MUI System 的版本在「解决同样的样式化问题」之外,额外提供了四个能力:
- 默认主题兜底:当 React 上下文中不存在 Theme 时,自动使用一套默认
theme,避免组件在无 Provider 环境下报错或样式缺失。 - 支持主题化配置:通过 options 里的
name,让theme.components[name].styleOverrides与theme.components[name].variants自动生效(可通过 option 关闭)。 - 内置
sx属性:生成的组件自动获得sx属性能力(可通过skipSx关闭)。 - 默认 prop 过滤:内置
shouldForwardProp(可覆盖),默认把ownerState、theme、sx、as这四个内部/特殊属性拦下,不转发给底层 DOM。
导入路径
@mui/system 与 @mui/material 都导出了这个工具,区别只在默认主题不同:
import { styled } from '@mui/system';
// 如果你在使用 @mui/material
import { styled } from '@mui/material/styles';
在仓库中可以同时验证两处导出:
- packages/mui-system/src/index.js:
export { default as styled } from './styled'; - packages/mui-material/src/styles/index.js:同样
export { default as styled } from './styled';,它从@mui/system汇聚并配套 Material 风格的主题与类型。
从源码看(packages/mui-system/src/createStyled/createStyled.js),@mui/system 的默认主题由 systemDefaultTheme = createTheme() 生成;@mui/material/styles 版本则会带上 Material Design 语义化调色板等更完整的主题结构。
API 详解:styled(Component, [options])(styles) => Component
这一调用约定是柯里化的:第一层接收被包装的组件与可选 options,第二层接收样式定义,最终返回新的组件。
参数 1:Component
被包装的组件。既可以是宿主标签字符串('div'、'button' 等),也可以是已有的 React 组件(包括 emotion 样式化组件)。
参数 2:options(对象,可选)
| 选项 | 类型 | 说明 |
|---|---|---|
shouldForwardProp |
(prop: string) => bool |
决定某个 prop 是否转发给底层 Component(例如避免把非 DOM 属性泄漏到 <div> 上) |
label |
string | 样式表的标签后缀,用于调试时定位样式来源 |
name |
string | theme.components 下读取 styleOverrides 与 variants 的键名;同时参与生成 label |
slot |
string | 若为 Root,会自动应用主题里的 variants |
overridesResolver |
(props, styles) => styles |
根据 props 与 theme.components[name].styleOverrides 决定返回哪些样式(常用于 color 等状态到样式槽位的映射) |
skipVariantsResolver |
bool | 关闭对 theme.components[name].variants 的自动解析 |
skipSx |
bool | 关闭生成组件上的 sx 属性 |
其余未列出的键会被原样转发给 emotion 的 styled([Component], [options])(例如 shouldForwardProp 之外的底层选项)。
参数 3:styles
styles: object | ({ ...props, theme }) => object
可以传一个静态样式对象;也可以传一个函数,它唯一的入参是一个同时包含 theme 与组件全部 props 的对象。
返回值
返回一个新的 React 组件。
options 各字段在源码中的默认行为
深入 packages/mui-system/src/createStyled/createStyled.js 可以看出各选项的“缺省推导”逻辑:
overridesResolver的默认值:当提供了slot时,默认解析器就是(_props, styles) => styles[slot]——即从styleOverrides里按小写化的 slot 取对应样式(Root→ 取styles.root,参见 createStyled.js#L37-L42)。若没有传slot,则默认为null,此时不会自动应用styleOverrides。skipVariantsResolver的默认值:root slot('Root'/'root')为false(即默认启用 variants 自动解析),其他非 root slot 为true;显式传入时以传入值为准(createStyled.js#L154-L160)。skipSx默认false,即默认启用sx。shouldForwardProp的默认值:见 createStyled.js#L20-L22,内置实现为源码里还会进一步细分:slot 为 Root 时走(prop) => prop !== 'ownerState' && prop !== 'theme' && prop !== 'sx' && prop !== 'as'rootShouldForwardProp,其他 slot 走slotShouldForwardProp;当没有 slot 且标签是宿主标签字符串时,为保持与 emotion/styled-components 行为一致,会置为undefined(createStyled.js#L164-L176)。- 调试友好的命名:非生产环境下,
displayName会生成为${componentName}${capitalize(componentSlot || '')}(如MyThemeComponentRoot),否则为Styled(${getDisplayName(tag)});label则形如${componentName}-${lowercaseFirstLetter(componentSlot || 'Root')}(createStyled.js#L313-L332)。这正是「在 DevTools 中看到类名以MyThemeComponent-root结尾」这一现象的来源。
基础用法
官方示例 BasicUsage.js 展示了最朴素的用法——直接对一个 div 宿主标签应用静态样式对象:
import { styled } from '@mui/system';
const MyComponent = styled('div')({
color: 'darkslategray',
backgroundColor: 'aliceblue',
padding: 8, // 会被转换为 `8px`
borderRadius: 4, // 会被转换为 `4px`
});
export default function BasicUsage() {
return <MyComponent>Styled div</MyComponent>;
}
两点值得留意:
- 这里的数字
padding: 8会被直接解释为8px(见下文「与sx的差异」小节),它不会经过theme.spacing。 - 宿主标签字符串 + 无 slot 时,组件上的未知 props 会按 emotion 默认规则处理,样式相关的
sx等则被默认过滤逻辑拦截。
使用主题:读取 theme 的响应式样式函数
styles 支持「函数形式」,函数会收到包含 theme 的对象。官方示例 ThemeUsage.js 演示了如何把主题令牌落到样式中:
import { styled, createTheme, ThemeProvider } from '@mui/system';
const customTheme = createTheme({
palette: {
primary: {
main: '#1976d2',
contrastText: 'white',
},
},
});
const MyThemeComponent = styled('div')(({ theme }) => ({
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(1),
borderRadius: theme.shape.borderRadius,
}));
export default function ThemeUsage() {
return (
<ThemeProvider theme={customTheme}>
<MyThemeComponent>Styled div with theme</MyThemeComponent>
</ThemeProvider>
);
}
这里 theme.spacing(1) 得到的是基于默认间距基准的 8px 级别度量,与 Material Design 的间距体系一致。
关于 theme 的来源,源码中有一段值得注意的处理:样式函数执行前,会先把 theme 附加到 props 上——若传入的 theme 为空对象则替换为默认主题;若定义了 themeId 则优先取作用域主题(createStyled.js#L44-L46)。因此即使在某个嵌套区域内没有任何 <ThemeProvider>,样式函数中的 theme 也不会是 undefined。
自定义组件:复刻核心组件的主题化能力
想要让自己的自定义组件拥有与核心组件一致的「主题可定制性」(styleOverrides + variants + sx),需要在 options 中声明 name、slot 并提供 overridesResolver。官方示例 UsingOptions.js 非常完整:
import { styled, createTheme, ThemeProvider } from '@mui/system';
const customTheme = createTheme({
components: {
MyThemeComponent: {
styleOverrides: {
root: {
color: 'darkslategray',
},
primary: {
color: 'darkblue',
},
secondary: {
color: 'darkred',
backgroundColor: 'pink',
},
},
variants: [
{
props: { variant: 'dashed', color: 'primary' },
style: { border: '1px dashed darkblue' },
},
{
props: { variant: 'dashed', color: 'secondary' },
style: { border: '1px dashed darkred' },
},
],
},
},
});
const MyThemeComponent = styled('div', {
// 配置哪些 props 应该转发到 DOM 上
shouldForwardProp: (prop) =>
prop !== 'color' && prop !== 'variant' && prop !== 'sx',
name: 'MyThemeComponent',
slot: 'Root',
// 指定 styleOverrides 如何基于 props 被应用
overridesResolver: (props, styles) => [
styles.root,
props.color === 'primary' && styles.primary,
props.color === 'secondary' && styles.secondary,
],
})(({ theme }) => ({
backgroundColor: 'aliceblue',
padding: theme.spacing(1),
}));
export default function UsingOptions() {
return (
<ThemeProvider theme={customTheme}>
<MyThemeComponent sx={{ m: 1 }} color="primary" variant="dashed">
Primary
</MyThemeComponent>
<MyThemeComponent sx={{ m: 1 }} color="secondary">
Secondary
</MyThemeComponent>
</ThemeProvider>
);
}
这个例子同时演示了四件事:
name+slot: 'Root'让组件接入了theme.components.MyThemeComponent;overridesResolver把colorprop 映射到styleOverrides中对应的root/primary/secondary槽位;variants中props与传入 props 匹配时,style会被自动追加;sx={{ m: 1 }}可用——m: 1在这里是sx专属的间距快捷值。
在开发模式下用浏览器 DevTools 检查这个元素,会看到生成的类名以 MyThemeComponent-root 结尾;同时 color、variant 这类只服务于样式的 props 并不会被泄漏到最终的 div 上——这正是 shouldForwardProp 的过滤结果:
上图源自文档页 styled.md,用于展示自定义组件经
name/slot处理后类名的实际呈现。
底层如何做到「主题化叠加」?
从源码看,createStyled 生成的最终样式解析器会把几类「表达式」按固定顺序拼接(createStyled.js#L216-L301):
- 头部:先注入
styleAttachTheme,确保 theme 已就位(对themeId/默认主题做解析); - 主体:开发者传入的样式表达式,每个表达式都会经过
transformStyle预处理——普通对象会被preprocessStyles预先编译、检测variants;函数会被包装成在渲染期调用并展开的样式处理器(createStyled.js#L184-L214); - 尾部:依次是
styleThemeOverrides(读取theme.components?.[componentName]?.styleOverrides,遍历槽位后用overridesResolver挑选并返回,见 createStyled.js#L225-L247)→styleThemeVariants(读取theme.components?.[componentName]?.variants,通过processStyleVariants逐条比对 props 后返回匹配样式,见 createStyled.js#L249-L263)→styleFunctionSx(sx的样式函数)。
variants 的匹配逻辑在 createStyled.js#L85-L119:若 variant.props 是函数则以「合并了 ownerState 的 props」作为入参调用它做判断;若是对象则逐一比对 props[key](兼容 props.ownerState[key]),全部相等才算命中。此外,当 theme.modularCssLayers 开启时,样式还会被包裹进 @layer components / @layer custom / @layer theme 等层级,以实现更可控的层叠顺序(组件名以 Mui 开头或提供了 slot 时归入 components 层,否则归入 custom 层,见 createStyled.js#L149-L152)。
值得补充的一点:当被包装的 tag 本身已经是样式化组件(组合场景)时,源码会先调用 mutateStyles 剔除其子样式中的 styleFunctionSx,避免复合组件重复生成 sx 相关的样式(createStyled.js#L133-L136)。
移除不需要的特性
如果希望自定义组件不要绑定 MUI System 的一些专属特性,官方文档给出了精确的开关写法:
const StyledComponent = styled('div', {}, {
name: 'MuiStyled',
slot: 'Root',
- overridesResolver: (props, styles) => styles.root, // 关闭 theme.components[name].styleOverrides
+ skipVariantsResolver: true, // 关闭 theme.components[name].variants
+ skipSx: true, // 关闭 sx 属性
});
需要理解这里的“移除”语义:
- 默认情况下
slot: 'Root'会自动生成overridesResolver,如果不想让theme.components[name].styleOverrides生效,需要显式地用一个返回空内容的解析器去覆盖它(如上方的删除线写法所注释); skipVariantsResolver: true则直接关掉 variants 自动解析(对 root slot 默认是开启的);skipSx: true关闭sx属性,此时expressionsTail中不会再被压入styleFunctionSx(见 createStyled.js#L265-L267)。
这样得到的组件在行为上会更贴近纯 emotion/styled-components 的 styled(),适合追求极致轻量、或明确不希望用户通过主题/sx 干预样式的场景。
创建自定义的 styled() 工具
若希望 styled() 使用与默认不同的主题作为兜底,可以用 createStyled() 派生一份自定义实现。官方示例:
import { createStyled, createTheme } from '@mui/system';
const defaultTheme = createTheme({
// 你的自定义主题值
});
const styled = createStyled({ defaultTheme });
export default styled;
createStyled 在 createStyled.js#L121-L127 中会接收这些入参:
themeId:用于区分作用域主题的标识;defaultTheme:默认兜底主题,缺省为systemDefaultTheme(createTheme()的结果);rootShouldForwardProp/slotShouldForwardProp:分别用于 root slot 与其余 slot 的 prop 过滤策略,默认都指向内置shouldForwardProp。
这种模式适合那些希望「全局默认主题由自己的设计系统决定」的团队:只需替换默认主题,而无需在每个组件处手动包裹 ThemeProvider。
styled() 与 sx 属性的差异
文档单独用一整节对比这两者,核心论断是:styled() 是函数,sx 是属性。styled() 保证对同一输入产出与底层样式库(emotion 或 styled-components)完全一致的样式结果,因为它就是对底层 styled() 的受控扩展;而 sx 是一种面向快速自定义的新式样式方案,只出现在用 styled() 创建的组件上。两者在语法上有四处明显差异。
1. sx 提供比 styled 更多的快捷值
mx: 1 这类间距速记(margin-inline)只在 sx 中可用:
const MyStyledButton = styled('button')({
mx: 1, // ❌ 不要这样用!该快捷值只由 sx 属性提供
});
import Button from '@mui/material/Button';
const MyStyledButton = (props) => (
<Button
sx={{
mx: 1, // ✔️ 该快捷值是 sx 专属
}}
>
{props.children}
</Button>
);
2. 样式值的“单位解释”不同
同样写 padding: 1,语义截然不同:
const MyStyledButton = styled('button')({
padding: 1, // 表示 "1px",而不是 "theme.spacing(1)"
});
import Button from '@mui/material/Button';
const MyStyledButton = (props) => (
<Button
sx={{
padding: 1, // 表示 "theme.spacing(1)",而不是 "1px"
}}
>
{props.children}
</Button>
);
也就是说:styled() 中数字单位走 CSS 直觉(px),sx 中数字会先经过 theme.spacing 换算——这也与 BasicUsage demo 里 padding: 8 注释为“会被转换为 8px”一致。
3. 使用 props 的模式不同
styled() 用顶层函数解构 props:
const MyStyledButton = styled('button')((props) => ({
backgroundColor: props.myBackgroundColor,
}));
styled-components / emotion 的老写法里也有「逐字段函数」:
// 你在社区代码里可能见过这种写法,但出于可读性,
// 我们建议只使用一个顶层函数
const MyStyledButtonPropsPerField = styled('button')({
backgroundColor: (props) => props.myBackgroundColor,
});
而 sx 是在组件内通过 prop 透传,且其回调收到的对象同时包含 theme:
import Button from '@mui/material/Button';
const MyStyledButton = (props) => (
<Button sx={{ backgroundColor: props.myCustomColor }}>{props.children}</Button>
);
4. 函数回调的“作用域”不同(sx 每个字段都能拿 theme)
在 sx 里,你可以针对单一字段书写基于 theme 的回调,甚至可以直接给主题色路径字符串:
import Button from '@mui/material/Button';
import { lighten } from 'polished';
const MyStyledButton = (props) => (
<Button
sx={{ backgroundColor: (theme) => lighten(0.2, theme.palette.primary.main) }}
>
{props.children}
</Button>
);
// 注:无需函数直接访问 theme 时,可以用字符串快捷写法:
const MyStyledButton = (props) => (
<Button sx={{ backgroundColor: 'primary.main' }}>{props.children}</Button>
);
'primary.main' 这种「主题路径字符串」会被 sx 内部解析成 theme.palette.primary.main,是 sx 高表达力的重要来源。
如何在 styled() 中享受 sx 语法
若你偏爱 sx 的写法,并希望把它同时用于 sx 属性与 styled() 样式,可以使用主题上的 unstable_sx 工具。官方示例 UsingWithSx.js:
import { styled, createTheme, ThemeProvider } from '@mui/system';
const customTheme = createTheme({
palette: {
primary: {
main: '#1976d2',
contrastText: 'white',
},
},
});
const MyThemeComponent = styled('div')(({ theme }) =>
theme.unstable_sx({
color: 'primary.contrastText',
backgroundColor: 'primary.main',
padding: 1,
borderRadius: 1,
}),
);
export default function UsingWithSx() {
return (
<ThemeProvider theme={customTheme}>
<MyThemeComponent>Styled div with theme</MyThemeComponent>
</ThemeProvider>
);
}
这里 padding: 1、color: 'primary.contrastText' 都遵循 sx 语义。该 unstable_sx 是在 createTheme 时挂到主题对象上的(参见 packages/mui-system/src/createTheme/createTheme.js#L39-L46 与类型定义 createTheme.d.ts#L51-L52),其底层即 styleFunctionSx。
官方文档强调:使用 unstable_sx 带来的额外开销与直接使用 sx 属性完全相同;并且 unstable_sx 并非只能在 styled() 里使用——例如定义自定义主题的 variants 样式时同样可以调用它。
提示:
sx与unstable_sx的完整语法、主题路径解析与响应式断点写法,见 the-sx-prop 文档。
组件选择器:在样式中引用另一个组件
emotion 与 styled-components 都支持「组件作为选择器」——例如让 Parent 内部的 Child 应用不同颜色:
import styled from '@emotion/styled';
const Child = styled.div`
color: red;
`;
const Parent = styled.div`
${Child} {
color: green;
}
`;
render(
<div>
<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>
</div>,
);
MUI System 的 styled() 同样支持这一写法,但分两种情况:
- 使用
@mui/styled-engine-sc(styled-components 引擎):无需任何额外配置,开箱即用; - 使用
@mui/styled-engine(emotion,默认引擎):需要几个额外步骤,让 emotion 的 Babel 插件识别出“MUI 版styled()”。
第一步,安装插件:
npm install @emotion/babel-plugin
第二步,在 Babel 配置中通过 importMap 告诉插件:@mui/system、@mui/material、@mui/material/styles 里的 styled 本质上就是 @emotion/styled 的默认导出,从而让组件插值语法在编译期被正确转换。
babel.config.js
module.exports = {
plugins: [
[
'@emotion',
{
importMap: {
'@mui/system': {
styled: {
canonicalImport: ['@emotion/styled', 'default'],
styledBaseImport: ['@mui/system', 'styled'],
},
},
'@mui/material': {
styled: {
canonicalImport: ['@emotion/styled', 'default'],
styledBaseImport: ['@mui/material', 'styled'],
},
},
'@mui/material/styles': {
styled: {
canonicalImport: ['@emotion/styled', 'default'],
styledBaseImport: ['@mui/material/styles', 'styled'],
},
},
},
},
],
],
};
提示:如果你同时使用
babel-plugin-direct-import,请把它放在@emotion/babel-plugin之后,以保证插件处理顺序正确。
配置完成后,就可以像 emotion 原版一样把 Child 组件直接嵌入 Parent 的模板字符串中使用组件选择器了。
总结与延伸阅读
围绕 MUI System 的 styled(),可以把知识归纳为三个层次:
- 使用层:
styled('div')({...})/styled(Comp, { name, slot, ... })的柯里化 API,以及theme、sx、variants 如何在自定义组件上完整复刻核心组件体验; - 机制层:内置默认主题、
shouldForwardProp过滤、styleOverrides/variants自动叠加、sx注入,全部由 createStyled.js 中「头部主题注入 + 主体样式表达式 + 尾部主题叠加与 sx」的表达式流水线实现; - 取舍层:用
skipVariantsResolver/skipSx裁剪功能、用createStyled换默认主题、以及牢记styled()(CSS 直觉单位)与sx(主题间距 + 快捷值)在语法语义上的差异。
如果还想继续深入,推荐按以下路径阅读当前仓库:
- 官方交互式示例:本主题的四个 demo(BasicUsage.js、ThemeUsage.js、UsingOptions.js、UsingWithSx.js)与它们的 TSX 版本位于 docs/data/system/styled;
sx的系统讲解与语法速查:the-sx-prop 文档;- 通过
theme.components全局定制组件的完整指南(styleOverrides / variants):theme-components 文档; - 核心实现:packages/mui-system/src/createStyled/createStyled.js(约 350 行,包含上述全部推导逻辑、
processStyle/processStyleVariants变体处理、@layer级联策略与 displayName/label 生成); - 主题与
unstable_sx的挂载点:packages/mui-system/src/createTheme/createTheme.js; - 各入口包的导出:packages/mui-system/src/index.js、packages/mui-material/src/styles/index.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
