首页
/ Material UI 的 `styled()` 指南:从基础用法到源码级原理详解

Material UI 的 `styled()` 指南:从基础用法到源码级原理详解

2026-09-06 18:30:00作者:尤辰城Agatha

导读

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 的版本在「解决同样的样式化问题」之外,额外提供了四个能力:

  1. 默认主题兜底:当 React 上下文中不存在 Theme 时,自动使用一套默认 theme,避免组件在无 Provider 环境下报错或样式缺失。
  2. 支持主题化配置:通过 options 里的 name,让 theme.components[name].styleOverridestheme.components[name].variants 自动生效(可通过 option 关闭)。
  3. 内置 sx 属性:生成的组件自动获得 sx 属性能力(可通过 skipSx 关闭)。
  4. 默认 prop 过滤:内置 shouldForwardProp(可覆盖),默认把 ownerStatethemesxas 这四个内部/特殊属性拦下,不转发给底层 DOM。

导入路径

@mui/system@mui/material 都导出了这个工具,区别只在默认主题不同:

import { styled } from '@mui/system';
// 如果你在使用 @mui/material
import { styled } from '@mui/material/styles';

在仓库中可以同时验证两处导出:

从源码看(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 下读取 styleOverridesvariants 的键名;同时参与生成 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,内置实现为
    (prop) => prop !== 'ownerState' && prop !== 'theme' && prop !== 'sx' && prop !== 'as'
    
    源码里还会进一步细分:slot 为 Root 时走 rootShouldForwardProp,其他 slot 走 slotShouldForwardProp;当没有 slot 且标签是宿主标签字符串时,为保持与 emotion/styled-components 行为一致,会置为 undefinedcreateStyled.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>;
}

两点值得留意:

  1. 这里的数字 padding: 8 会被直接解释为 8px(见下文「与 sx 的差异」小节),它不会经过 theme.spacing
  2. 宿主标签字符串 + 无 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 中声明 nameslot 并提供 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
  • overridesResolvercolor prop 映射到 styleOverrides 中对应的 root / primary / secondary 槽位;
  • variantsprops 与传入 props 匹配时,style 会被自动追加;
  • sx={{ m: 1 }} 可用——m: 1 在这里是 sx 专属的间距快捷值。

在开发模式下用浏览器 DevTools 检查这个元素,会看到生成的类名以 MyThemeComponent-root 结尾;同时 colorvariant 这类只服务于样式的 props 并不会被泄漏到最终的 div 上——这正是 shouldForwardProp 的过滤结果:

浏览器 DevTools 中渲染结果示意

上图源自文档页 styled.md,用于展示自定义组件经 name/slot 处理后类名的实际呈现。

底层如何做到「主题化叠加」?

从源码看,createStyled 生成的最终样式解析器会把几类「表达式」按固定顺序拼接(createStyled.js#L216-L301):

  1. 头部:先注入 styleAttachTheme,确保 theme 已就位(对 themeId/默认主题做解析);
  2. 主体:开发者传入的样式表达式,每个表达式都会经过 transformStyle 预处理——普通对象会被 preprocessStyles 预先编译、检测 variants;函数会被包装成在渲染期调用并展开的样式处理器(createStyled.js#L184-L214);
  3. 尾部:依次是 styleThemeOverrides(读取 theme.components?.[componentName]?.styleOverrides,遍历槽位后用 overridesResolver 挑选并返回,见 createStyled.js#L225-L247)→ styleThemeVariants(读取 theme.components?.[componentName]?.variants,通过 processStyleVariants 逐条比对 props 后返回匹配样式,见 createStyled.js#L249-L263)→ styleFunctionSxsx 的样式函数)。

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;

createStyledcreateStyled.js#L121-L127 中会接收这些入参:

  • themeId:用于区分作用域主题的标识;
  • defaultTheme:默认兜底主题,缺省为 systemDefaultThemecreateTheme() 的结果);
  • 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: 1color: '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 样式时同样可以调用它。

提示:sxunstable_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(),可以把知识归纳为三个层次:

  1. 使用层styled('div')({...}) / styled(Comp, { name, slot, ... }) 的柯里化 API,以及 themesx、variants 如何在自定义组件上完整复刻核心组件体验;
  2. 机制层:内置默认主题、shouldForwardProp 过滤、styleOverrides/variants 自动叠加、sx 注入,全部由 createStyled.js 中「头部主题注入 + 主体样式表达式 + 尾部主题叠加与 sx」的表达式流水线实现;
  3. 取舍层:用 skipVariantsResolver / skipSx 裁剪功能、用 createStyled 换默认主题、以及牢记 styled()(CSS 直觉单位)与 sx(主题间距 + 快捷值)在语法语义上的差异。

如果还想继续深入,推荐按以下路径阅读当前仓库:

登录后查看全文
热门项目推荐
相关项目推荐