首页
/ Material UI 创建主题化自定义组件:Slots、ownerState 与 useThemeProps 完整实践

Material UI 创建主题化自定义组件:Slots、ownerState 与 useThemeProps 完整实践

2026-09-06 14:12:43作者:卓艾滢Kingsley

本文基于 Material UI 官方文档「Creating themed components」,讲解如何把你的自定义组件接入 Material UI 的主题系统,使其像内置组件一样接受 components 配置(styleOverridesvariantsdefaultProps)。通过本篇指南,你将掌握:用 styled API 的 name/slot 参数定义组件插槽(slots)、用 ownerState 将 prop 传入插槽参与样式计算、用 useThemeProps 支持主题默认 props,以及完整的 TypeScript 类型接入方案,最终产出一个可跨项目复用的 Stat 统计组件模板。

这套方案适用于在 Material UI 之上构建组件库的团队——组件一旦按此规范开发,就可以在任何消费方的 createTheme 中直接定制。文档同时给出了一条务实建议:如果你的组件只在单个项目中使用,其实并不需要将组件接入主题系统;只有需要跨项目可主题化(themeable)时才值得走完整流程。

1. 组件插槽(Slots):让每个元素可被主题定位

Material UI 的主题系统通过 theme 的 styleOverridesvariants 两个配置点来定制组件外观,而这两个配置都以「组件名 + 插槽名」为定位坐标。因此第一步是为自定义组件定义插槽。

以文档中的统计组件为例,它由三个插槽组成:

  • root:组件的容器元素;
  • value:统计数值;
  • unit:统计单位的说明文字。

官方建议:无论插槽叫什么名字,最外层容器元素统一命名为 root,以保持与库内其余组件的一致性。

使用 styled API 并传入 name(组件名)和 slot(插槽名)两个选项来创建插槽:

import * as React from 'react';
import { styled } from '@mui/material/styles';

const StatRoot = styled('div', {
  name: 'MuiStat', // The component name
  slot: 'root', // The slot name
})(({ theme }) => ({
  display: 'flex',
  flexDirection: 'column',
  gap: theme.spacing(0.5),
  padding: theme.spacing(3, 4),
  backgroundColor: theme.palette.background.paper,
  borderRadius: theme.shape.borderRadius,
  boxShadow: theme.shadows[2],
  letterSpacing: '-0.025em',
  fontWeight: 600,
  ...theme.applyStyles('dark', {
    backgroundColor: 'inherit',
  }),
}));

const StatValue = styled('div', {
  name: 'MuiStat',
  slot: 'value',
})(({ theme }) => ({
  ...theme.typography.h3,
}));

const StatUnit = styled('div', {
  name: 'MuiStat',
  slot: 'unit',
})(({ theme }) => ({
  ...theme.typography.body2,
  color: theme.palette.text.secondary,
}));

这三个 styled 组件都声明 name: 'MuiStat',意味着它们共享同一个主题定制命名空间,仅靠 slot 区分具体元素。

从源码结构看,name/slot 参数的处理位于 createStyled 实现styled 工厂函数接收 taginputOptions,从中解析出组件名与插槽名。此外,仓库中的 完整模板 还在插槽样式里直接使用了 variants 数组(variant: 'outlined' 时加边框、去掉阴影),这与文档分步指南中通过 ownerState 条件展开的方式是等价的两种写法——variants 写法可以让样式随主题被解析并支持 CSS 层叠,而条件展开写法对内部状态更灵活。

2. 组装组件并应用主题

有了插槽之后,用 React.forwardRef 把三个插槽组装成完整组件:

// /path/to/Stat.js
import * as React from 'react';

const StatRoot = styled('div', {
  name: 'MuiStat',
  slot: 'root',
})(…);

const StatValue = styled('div', {
  name: 'MuiStat',
  slot: 'value',
})(…);

const StatUnit = styled('div', {
  name: 'MuiStat',
  slot: 'unit',
})(…);

const Stat = React.forwardRef(function Stat(props, ref) {
  const { value, unit, ...other } = props;

  return (
    <StatRoot ref={ref} {...other}>
      <StatValue>{value}</StatValue>
      <StatUnit>{unit}</StatUnit>
    </StatRoot>
  );
});

export default Stat;

此时消费方就可以像定制内置组件一样,在 createTheme 中按「组件名 → 插槽名」定位样式:

import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  components: {
    // the component name defined in the `name` parameter
    // of the `styled` API
    MuiStat: {
      styleOverrides: {
        // the slot name defined in the `slot` and `overridesResolver` parameters
        // of the `styled` API
        root: {
          backgroundColor: '#121212',
        },
        value: {
          color: '#fff',
        },
        unit: {
          color: '#888',
        },
      },
    },
  },
});

关键在于 styleOverrides 的键必须与 styled API 中 slot 参数一一对应,MuiStat 则对应 name 参数——这就是第 1 步声明 name/slot 的意义所在:它建立了主题配置与 DOM 元素之间的映射。

3. 用 ownerState 让插槽感知组件 prop

当组件有 variant 这类需要影响内部插槽样式的 prop 时,直接解构传递会导致插槽无法读取到它(它不会被 spread 到需要它的地方)。官方方案是:把需要样式化的 prop 和内部状态封装进 ownerState 对象,作为 prop 传给每个插槽。

ownerState 是一个特殊名称,styled API 不会把它透传到 DOM,因此它只参与样式计算、不污染渲染结果。先给 Stat 增加 variant prop:

  const Stat = React.forwardRef(function Stat(props, ref) {
+   const { value, unit, variant, ...other } = props;
+
+   const ownerState = { ...props, variant };

    return (
-      <StatRoot ref={ref} {...other}>
-        <StatValue>{value}</StatValue>
-        <StatUnit>{unit}</StatUnit>
-      </StatRoot>
+      <StatRoot ref={ref} ownerState={ownerState} {...other}>
+        <StatValue ownerState={ownerState}>{value}</StatValue>
+        <StatUnit ownerState={ownerState}>{unit}</StatUnit>
+      </StatRoot>
    );
  });

然后在插槽内读取 ownerState,基于 variant 计算样式:

  const StatRoot = styled('div', {
    name: 'MuiStat',
    slot: 'root',
-  })(({ theme }) => ({
+  })(({ theme, ownerState }) => ({
    display: 'flex',
    flexDirection: 'column',
    gap: theme.spacing(0.5),
    padding: theme.spacing(3, 4),
    backgroundColor: theme.palette.background.paper,
    borderRadius: theme.shape.borderRadius,
    boxShadow: theme.shadows[2],
    letterSpacing: '-0.025em',
    fontWeight: 600,
    ...theme.applyStyles('dark', {
      backgroundColor: 'inherit',
    }),
+   ...ownerState.variant === 'outlined' && {
+    border: `2px solid ${theme.palette.divider}`,
+   },
  }));

「ownerState 不会 spread 到 DOM」这一点在源码中有明确依据:createStyled 中的属性过滤函数会显式排除 ownerStatethemesxas 这几个属性,保证它们只留在样式层。另外从源码结构看,processStyleVariants同文件)在匹配 variants 配置时会合并 propsprops.ownerState 后再比较 variant.props——这解释了为什么在模板文件里 variantsprops: { variant: 'outlined' } 能直接命中 variant prop,也印证了 ownerState 是插槽样式访问组件状态的标准通道。

4. 支持主题默认 props(useThemeProps)

不同消费项目可能希望以主题为单位定制组件的默认 props(例如全局默认 variant: 'outlined')。这一步必须使用 useThemeProps API:

+ import { useThemeProps } from '@mui/material/styles';

- const Stat = React.forwardRef(function Stat(props, ref) {
+ const Stat = React.forwardRef(function Stat(inProps, ref) {
+   const props = useThemeProps({ props: inProps, name: 'MuiStat' });
    const { value, unit, ...other } = props;

    return (
      <StatRoot ref={ref} {...other}>
        <StatValue>{value}</StatValue>
        <StatUnit>{unit}</StatUnit>
      </StatRoot>
    );
  });

name: 'MuiStat' 必须与 styledname 保持一致,这样主题中该组件条目下的 defaultProps 才会被合入。之后消费方即可在主题里覆盖默认值:

import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiStat: {
      defaultProps: {
        variant: 'outlined',
      },
    },
  },
});

从实现看,@mui/material 的 useThemeProps 是对 mui-system 版本 的封装:它先通过 useTheme 取到当前主题(支持 themeId 命名空间),再调用 getThemeProps({ theme, name, props }) 完成「主题 defaultProps 打底 + 传入 props 覆盖」的合并。这也意味着 useThemeProps 必须在 forwardRef 回调内部调用,以保证始终读取当前主题上下文。

5. TypeScript 接入:Props、ownerState 与模块声明

使用 TypeScript 时,需要为组件 props 与 ownerState 分别定义接口:

interface StatProps {
  value: number | string;
  unit: string;
  variant?: 'outlined';
}

interface StatOwnerState extends StatProps {
  // …key value pairs for the internal state that you want to style the slot
  // but don't want to expose to the users
}

StatOwnerStateStatProps 基础上扩展——既可以复用对外的 prop,也可以加入「只想用于插槽样式、不想暴露给用户」的内部状态键值对。然后把类型注入插槽与组件:

const StatRoot = styled('div', {
  name: 'MuiStat',
  slot: 'root',
})<{ ownerState: StatOwnerState }>(({ theme, ownerState }) => ({
  display: 'flex',
  flexDirection: 'column',
  gap: theme.spacing(0.5),
  padding: theme.spacing(3, 4),
  backgroundColor: theme.palette.background.paper,
  borderRadius: theme.shape.borderRadius,
  boxShadow: theme.shadows[2],
  letterSpacing: '-0.025em',
  fontWeight: 600,
  ...theme.applyStyles('dark', {
    backgroundColor: 'inherit',
  }),
  // typed-safe access to the `variant` prop
  ...(ownerState.variant === 'outlined' && {
    border: `2px solid ${theme.palette.divider}`,
    boxShadow: 'none',
  }),
}));

// …do the same for other slots

const Stat = React.forwardRef<HTMLDivElement, StatProps>(function Stat(inProps, ref) {
  const props = useThemeProps({ props: inProps, name: 'MuiStat' });
  const { value, unit, variant, ...other } = props;

  const ownerState = { ...props, variant };

  return (
    <StatRoot ref={ref} ownerState={ownerState} {...other}>
      <StatValue ownerState={ownerState}>{value}</StatValue>
      <StatUnit ownerState={ownerState}>{unit}</StatUnit>
    </StatRoot>
  );
});

注意 styled 调用后的泛型参数 <{ ownerState: StatOwnerState }> 让插槽内对 ownerState.variant 的访问获得类型安全;组件本体的 forwardRef<HTMLDivElement, StatProps> 则保证 ref 与 props 的类型正确。

最后一步是把 Stat 注册进主题类型系统,使消费方在 createTheme 中写 MuiStat 配置时能获得完整的类型提示与校验:

import {
  ComponentsOverrides,
  ComponentsVariants,
  Theme as MuiTheme,
} from '@mui/material/styles';
import { StatProps } from 'path/to/Stat';

type Theme = Omit<MuiTheme, 'components'>;

declare module '@mui/material/styles' {
  interface ComponentNameToClassKey {
    MuiStat: 'root' | 'value' | 'unit';
  }

  interface ComponentsPropsList {
    MuiStat: Partial<StatProps>;
  }

  interface Components {
    MuiStat?: {
      defaultProps?: ComponentsPropsList['MuiStat'];
      styleOverrides?: ComponentsOverrides<Theme>['MuiStat'];
      variants?: ComponentsVariants['MuiStat'];
    };
  }
}

三处声明各司其职:ComponentNameToClassKey 声明该组件拥有的插槽集合(对应生成的 class 键);ComponentsPropsList 声明 defaultProps 的合法取值;Components 接口把 MuiStat 正式加入 components 配置类型,使其 styleOverrides/variants 均受类型约束。

6. 完整模板与验证

上述四步的完整产物即仓库内的 Stat 组件模板(JS 版)TypeScript 版,可直接作为自建主题化组件的起点。模板要点回顾:

  1. 三个插槽统一声明 name: 'MuiStat',分别使用 slot: 'root' | 'value' | 'unit'
  2. 插槽样式中直接携带 variants 数组,将 variant: 'outlined' 映射为边框样式;
  3. 组件内先经 useThemeProps({ props: inProps, name: 'MuiStat' }) 合并主题默认 props,再构造 ownerState 并下发到每个插槽;
  4. JS 版附带 propTypes 声明(value: number | stringunit: stringvariant: 'outlined'),便于运行时校验。

最终效果:消费方既可以在主题里写 styleOverrides.root/value/unit 覆盖外观,也可以写 defaultProps.variant 改变默认行为——自定义组件在 API 层面与内置组件完全同构。

7. 适用前提与小结

  • 适用前提:方案基于 @mui/material/styles 导出的 styledcreateThemeuseThemeProps,适用于 Material UI 当前的主题系统;单项目内使用的简单组件可跳过整条主题化流程。
  • 四个关键约定name 对应主题 components 键名、slot 对应 styleOverrides 键名、ownerState 承载插槽可见的组件状态且不透传 DOM、useThemePropsname 必须与 styledname 一致。
  • 可继续深入的路径styled API 源码name/slot/overridesResolver 的解析逻辑、useThemeProps 实现 的主题合并流程,以及官方 Theme components 文档styleOverridesvariants 的完整配置格式。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388