首页
/ Material UI 组件样式定制策略全解:从 sx 属性到 GlobalStyles 全局覆盖的实战指南

Material UI 组件样式定制策略全解:从 sx 属性到 GlobalStyles 全局覆盖的实战指南

2026-09-06 21:04:07作者:凤尚柏Louis

本篇指南基于 Material UI 官方文档《How to customize》展开,系统讲解四种由窄到宽的组件样式定制策略:一次性覆盖(sx 属性)、可复用组件(styled())、全局主题覆盖与全局 CSS 覆盖。读完本文,你将能够针对具体场景选择正确的定制层级,掌握不稳定哈希类名与稳定全局类名的区别,理解状态类(state classes)的特异性机制,并学会用 GlobalStyles 组件和 MuiCssBaseline 槽位覆盖 HTML 元素基线样式,且每种写法都附有可复制运行的完整代码。

定制层级总览:按“影响范围”选择策略

Material UI 提供了多种定制组件样式的方式,你的具体场景决定了哪种方式最合适。官方文档按使用范围从窄到宽给出了四个层级,本文的结构与之一致:

  1. 一次性定制(One-off customization):只改变某个组件的单一实例
  2. 可复用组件(Reusable component):在不同位置复用同一组覆盖
  3. 全局主题覆盖(Global theme overrides):通过 theme 统一管理所有组件的样式一致性
  4. 全局 CSS 覆盖(Global CSS override):覆盖 HTML 元素(如 h1body)的基线样式

一个实用的辅助手段:仓库中内置了面向 AI 编码助手的 styling agent skill(位于 skills/material-ui-styling/SKILL.md),其中给出了在 sxstyled()、theme overrides 与全局 CSS 之间做选择的决策指引,可配合本文一起使用。

1. 一次性定制:只改一个实例

当只需要修改组件的某一个实例时,有三种可选途径:sx 属性、嵌套类名覆盖、className 自定义类。

1.1 sx 属性:单实例覆盖的首选方案

sx 属性在绝大多数场景下是为单个组件实例添加样式覆盖的最佳选择,它可用于所有 Material UI 组件。官方演示 SxProp.tsx 展示了最基础的用法:

import Slider from '@mui/material/Slider';

export default function SxProp() {
  return <Slider defaultValue={30} sx={{ width: 300, color: 'success.main' }} />;
}

注意 color: 'success.main' 的写法——sx 属性支持主题键值引用,success.main 会从当前 theme 的 palette 中解析出实际颜色,这比硬编码色值更符合主题化规范。

1.2 用嵌套类名覆盖组件的内部槽位

有时你想定制的不是组件整体,而是它的某个局部(slot)。例如把 Slider 的拇指(thumb)从圆形改成方形。官方给出的工作流是:

第一步:在浏览器开发者工具中定位目标槽位的类名。

Material UI 注入 DOM 的样式依赖遵循标准命名模式的类名:

[hash]-Mui[组件名]-[槽位名]

浏览器开发者工具中定位 MuiSlider-thumb 类名的截图

Slider 拇指的例子中,实际应用的类名是 .css-ae2u5c-MuiSlider-thumb,但你真正需要关注的只是 .MuiSlider-thumb——其中 Slider 是组件名,thumb 是槽位名。然后用它构造 sx 属性内的 CSS 选择器(& .MuiSlider-thumb),这就是官方演示 DevTools.tsx 的完整实现:

import Slider from '@mui/material/Slider';

export default function DevTools() {
  return (
    <Slider
      defaultValue={30}
      sx={{
        width: 300,
        color: 'success.main',
        '& .MuiSlider-thumb': {
          borderRadius: '1px',
        },
      }}
    />
  );
}

这里 & 指向组件根节点,& .MuiSlider-thumb 即以根节点为上下文选择内部拇指元素。

重要警告css-ae2u5c 这类带哈希前缀的完整类名不稳定(哈希值随构建变化),绝不能直接作为 CSS 选择器使用。请始终只依赖 Mui[组件名]-[槽位] 这一段稳定的全局类名。

1.3 用自定义 className 覆盖样式

如果偏好用自己的类(而不是 sx)来覆盖组件样式,可以使用每个组件都支持的 className 属性。要覆盖组件的特定部分,同样应使用上一节介绍的 Material UI 全局类名(如 .MuiSlider-thumb)。文档同时指向了样式库互操作指南,其中演示了与 Tailwind、Sass、Vanilla Extract 等不同样式库配合该模式的具体做法。

1.4 状态类(State classes)与 CSS 特异性

hoverfocusdisabledselected 这类状态样式使用了更高的 CSS 特异性(specificity)。因此要定制它们,你必须提高自身选择器的特异性

Buttondisabled 状态为例,可以借助伪类 :disabled(它在 Web 规范中真实存在):

.Button {
  color: black;
}

/* 提高特异性 */
.Button:disabled {
  color: white;
}
<Button disabled className="Button">

但并非所有状态都能用 CSS 伪类表达——例如 MenuItemselected 状态,Web 规范中并不存在对应伪类。此时应使用 Material UI 提供的状态类(state classes),它们的行为类似 CSS 伪类。针对 MenuItem 的 selected 状态,目标全局类名是 .Mui-selected

.MenuItem {
  color: black;
}

/* 提高特异性 */
.MenuItem.Mui-selected {
  color: blue;
}
<MenuItem selected className="MenuItem">

为什么覆盖单个状态需要提高特异性?

CSS 伪类本身具有较高的特异性等级。为了与原生元素保持一致的行为,Material UI 的状态类被赋予了与 CSS 伪类相同的特异性等级,这使得你可以单独针对某个组件的某种状态而不影响其他状态。

Material UI 提供了哪些状态类?

可以从源码结构印证这一点:Mui-selectedMui-focusVisibleMui-active 等类名在 ButtonBase.jsTab.jsToggleButton.js 等组件实现中直接生成并附加到 DOM 上。可用的全局状态类名如下:

状态 全局类名
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

错误用法警示:永远不要直接对状态类名应用样式,例如 .Mui-error { color: red; } 会波及所有带该状态类的组件,产生难以排查的副作用。正确的做法是始终把状态类与某个组件的类名组合使用:

/* ❌ NOT OK */
.Mui-error {
  color: red;
}

/* ✅ OK */
.MuiOutlinedInput-root.Mui-error {
  color: red;
}

2. 可复用组件:用 styled() 沉淀跨位置的覆盖

当同一组覆盖需要在应用中多处复用时,应当创建可复用组件,官方推荐的工具是 styled()。官方演示 StyledCustomization.tsx 展示了完整的“成功色 Slider”:

import Slider, { SliderProps } from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';

const SuccessSlider = styled(Slider)<SliderProps>(({ 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} />;
}

这里体现了两个关键点:styled() 回调中可以直接拿到 theme(使用 theme.palette.success.main 而非硬编码颜色);以及 1.2 节学到的槽位类名模式(& .MuiSlider-thumb)在 styled() 中同样适用。alpha() 工具则用于生成带透明度的光晕阴影。

2.1 动态覆盖(Dynamic overrides)

styled() 允许基于组件的 props 添加动态样式,官方给出了两种实现路径:动态 CSSCSS 变量

方案一:动态 CSS(variants)

TypeScript 注意:如果你使用 TypeScript,需要为新组件补充新增 prop 的类型定义。

官方演示 DynamicCSS.tsx 使用 variants API 实现,通过 props 回调根据 prop 值条件性地应用样式块:

import * as React from 'react';
import { alpha, styled } from '@mui/material/styles';
import Slider, { SliderProps } from '@mui/material/Slider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';

interface StyledSliderProps extends SliderProps {
  success?: boolean;
}

const StyledSlider = styled(Slider, {
  shouldForwardProp: (prop) => prop !== 'success',
})<StyledSliderProps>(({ 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)}`,
          },
        },
      },
    },
  ],
}));

export default function DynamicCSS() {
  const [success, setSuccess] = React.useState(false);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSuccess(event.target.checked);
  };

  return (
    <React.Fragment>
      <FormControlLabel
        control={
          <Switch
            checked={success}
            onChange={handleChange}
            color="primary"
            value="dynamic-class-name"
          />
        }
        label="Success"
      />
      <StyledSlider success={success} defaultValue={30} sx={{ mt: 1 }} />
    </React.Fragment>
  );
}

两个细节值得留意:

  • shouldForwardProp: (prop) => prop !== 'success' 确保自定义的 success prop 不会被透传到 DOM 元素上,避免 React 的 “unknown prop” 警告;
  • 文档正文中还给出了一种更简洁的等价写法(不用 variants 而直接在样式函数中做条件展开):
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Slider, { SliderProps } from '@mui/material/Slider';

interface StyledSliderProps extends SliderProps {
  success?: boolean;
}

const StyledSlider = styled(Slider, {
  shouldForwardProp: (prop) => prop !== 'success',
})<StyledSliderProps>(({ success, theme }) => ({
  ...(success &&
    {
      // 使用新 prop 时添加的覆盖
    }),
}));

方案二:CSS 变量

官方演示 DynamicCSSVariables.tsx 展示了另一条路径:组件样式固定引用 CSS 变量,运行时只需切换变量值即可改变外观——

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

const CustomSlider = styled(Slider)({
  width: 300,
  color: 'var(--color)',
  '& .MuiSlider-thumb': {
    [`&:hover, &.Mui-focusVisible`]: {
      boxShadow: '0px 0px 0px 8px var(--box-shadow)',
    },
    [`&.Mui-active`]: {
      boxShadow: '0px 0px 0px 14px var(--box-shadow)',
    },
  },
});

const successVars = {
  '--color': '#4caf50',
  '--box-shadow': 'rgb(76, 175, 80, .16)',
} as React.CSSProperties;

const defaultVars = {
  '--color': '#1976d2',
  '--box-shadow': 'rgb(25, 118, 210, .16)',
} as React.CSSProperties;

export default function DynamicCSSVariables() {
  const [vars, setVars] = React.useState<React.CSSProperties>(defaultVars);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setVars(event.target.checked ? successVars : defaultVars);
  };

  return (
    <React.Fragment>
      <FormControlLabel
        control={
          <Switch
            checked={vars === successVars}
            onChange={handleChange}
            color="primary"
            value="dynamic-class-name"
          />
        }
        label="Success"
      />
      <CustomSlider style={vars} defaultValue={30} sx={{ mt: 1 }} />
    </React.Fragment>
  );
}

CSS 变量方案的优势在于样式类只生成一次(切换变量不产生新的样式注入),适合状态切换频繁、对运行时开销敏感的场景。

3. 全局主题覆盖:用 theme 管理组件级一致性

Material UI 提供了主题工具,用于管理整个用户界面中所有组件之间的样式一致性。该层级通过 createTheme()components 配置项,对指定组件的根节点或槽位做全局 styleOverrides,作用范围介于“可复用组件”与“全局 CSS”之间。完整的 API 说明参见仓库中的组件主题定制页面,例如:

const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        root: { borderRadius: 12 },
      },
    },
  },
});

4. 全局 CSS 覆盖:GlobalStylesCssBaseline

要为部分 HTML 元素添加全局基线样式,应使用 GlobalStyles 组件。从源码实现看(GlobalStyles.js),@mui/material/GlobalStyles 本身是一层薄封装,将 defaultThemethemeId 注入到 @mui/system 的同名组件:

function GlobalStyles(props) {
  return <SystemGlobalStyles {...props} defaultTheme={defaultTheme} themeId={THEME_ID} />;
}

styles 属性接受对象、字符串、数组、布尔值、数字、函数或混合形式(见同文件的 PropTypes 定义)。

4.1 基础用法:覆盖 h1 元素

官方演示 GlobalCssOverride.tsx

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>
  );
}

4.2 用回调访问 theme

styles 属性支持回调函数形式,以便在样式中访问主题。官方演示 GlobalCssOverrideTheme.tsx

import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';

export default function GlobalCssOverrideTheme() {
  return (
    <React.Fragment>
      <GlobalStyles
        styles={(theme) => ({
          h1: { color: theme.palette.primary.main },
        })}
      />
      <h1>Grey h1 element</h1>
    </React.Fragment>
  );
}

4.3 与 CssBaseline 协作:把全局样式并入基线

如果你的项目中已经使用 CssBaseline 组件设置基线样式,更好的做法是把这类全局样式作为对 MuiCssBaseline 组件槽位的覆盖写入 theme,而不是再挂一个独立的 GlobalStyles。官方演示 OverrideCssBaseline.tsx

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>
  );
}

MuiCssBaselinestyleOverrides 同样支持回调形式以访问 theme。官方演示 OverrideCallbackCssBaseline.tsx

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

const theme = createTheme({
  palette: {
    success: {
      main: '#ff0000',
    },
  },
  components: {
    MuiCssBaseline: {
      styleOverrides: (themeParam) => `
        h1 {
          color: ${themeParam.palette.success.main};
        }
      `,
    },
  },
});

export default function OverrideCallbackCssBaseline() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <h1>h1 element</h1>
    </ThemeProvider>
  );
}

4.4 性能实践:把 GlobalStyles 提升为静态常量

官方建议(success 提示框原文):<GlobalStyles /> 提升为静态常量是良好实践,可以避免重渲染——这样能保证它生成的 <style> 标签不会在每次渲染时重新计算。文档给出的 diff 示例:

 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>
   )
 }

关键点在于:写在函数体外的 <GlobalStyles /> 元素是模块级常量,其引用在多次渲染间保持不变,样式注入层因此可以跳过重复的样式计算;而每次渲染现场创建的 <GlobalStyles styles={...} /> 则是全新的 React 元素,会触发样式重新求值。

策略选型速查

场景 推荐策略 关键依据
单个实例的一次性样式 sx 属性 全组件通用,支持主题键值
定制组件内部槽位(如 Slider thumb) sx 中用 & .Mui[组件]-[槽位] 选择器 只依赖稳定的全局类名,禁用哈希类名
覆盖 hover/disabled/selected 等状态 组合“组件类 + 状态类”提高特异性 状态类特异性等同伪类,勿单独使用 .Mui-*
多处复用的固定覆盖 styled() 组件 回调中可访问 theme
基于 prop 的动态样式 styled() + variants 或 CSS 变量 配合 shouldForwardProp 防止 prop 泄漏到 DOM
全应用组件级一致性 theme 的 components.*.styleOverrides 参见组件主题定制文档
HTML 元素基线样式 GlobalStylesMuiCssBaseline.styleOverrides 优先并入 CssBaseline,并提升为静态常量

所有演示源码均位于 docs/data/material/customization/how-to-customize/ 目录,可直接对照本文逐段验证;GlobalStyles 的封装实现见 packages/mui-material/src/GlobalStyles/GlobalStyles.js

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