首页
/ Material UI 主题定制深度实践:createTheme、ThemeProvider 与 CSS 主题变量全解

Material UI 主题定制深度实践:createTheme、ThemeProvider 与 CSS 主题变量全解

2026-09-06 17:13:52作者:幸俭卉

本篇技术指南围绕 Material UI 仓库中的官方 Theming 文档(theming.md)展开,系统讲解如何用 createTheme 创建主题、用 ThemeProvider 将主题注入组件树、通过 useTheme 在组件内读取主题变量,以及启用 CSS 主题变量的完整方案。读完本文,你将能够独立完成一套符合品牌规范的 Material UI 主题配置,理解主题嵌套、className/style 合并等进阶行为,并掌握 responsiveFontSizesenhanceHighContrast 两个主题增强函数的用法。

主题(Theme)的核心定位

主题规定了组件的颜色、表面的明暗程度、阴影层级、墨色元素的透明度等设计要素。通过主题,你可以为整个应用施加一致的视觉基调,定制化项目的所有设计层面,以满足业务或品牌的特定需求。

为了在不同应用之间获得更大的一致性,Material UI 提供 light 和 dark 两种主题类型供选择。默认情况下,组件使用 light 类型。

从源码结构看,主题的构建入口是 createTheme.ts。它在收到 options 后会做三件事:

  1. 解析 palettecssVariablescolorSchemesdefaultColorScheme 等顶层选项;
  2. 若未启用 CSS 变量(cssVariables 为 false 且未声明 colorSchemes),走与 v5 完全一致的 createThemeNoVars.js 分支,行为与历史版本兼容;
  3. 若声明了 colorSchemes 或启用了 cssVariables,则进入 createThemeWithVars.js 分支,生成带 CSS 变量的主题,并处理 light/dark 双套色板(color scheme)的组装。

createThemeNoVars 内部会用 deepmerge 把用户传入的选项与默认主题合并,并逐项补全 mixinspaletteshadowstypographytransitionszIndex 等缺失部分——这正是“传入不完整主题对象、自动补齐缺失部分”这一 API 承诺的实现基础。

ThemeProvider:把主题注入组件树

Material UI 组件开箱即贴合库的默认主题。要注入自定义主题,需要使用 ThemeProvider。它依赖 React 的 Context 机制把主题向下传递给组件,因此必须保证 ThemeProvider 是你要定制组件的祖先节点。

ThemeProvider 接收 theme prop,并将其应用到它所包裹的整个 React 子树;推荐把它放在组件树的根部。其 Props 如下:

名称 类型 说明
children * node 你的组件树
theme * union: object | func 主题对象,通常是 createTheme() 的结果。提供的主题会与默认主题合并。也可以提供一个函数来扩展外层主题

最小示例:

import * as React from 'react';
import { red } from '@mui/material/colors';
import { ThemeProvider, createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      main: red[500],
    },
  },
});

function App() {
  return <ThemeProvider theme={theme}>...</ThemeProvider>;
}

除了文档列出的两个基础 prop,从 ThemeProvider.tsx 的源码定义可以看到,它在启用 CSS 主题变量时还暴露了一批实用 props:

Prop 默认值 作用
defaultMode 'system' 本地存储中尚无模式记录时的默认模式,要求主题定义了 light 和 dark 两套 colorSchemes
modeStorageKey 'mui-mode' 存储应用模式(mode)的 localStorage key
colorSchemeStorageKey 'mui-color-scheme' 存储 color scheme 的 localStorage key
colorSchemeNode document 挂载 theme.colorSchemeSelector 的节点
disableStyleSheetGeneration false 禁止生成 CSS 主题变量样式表,用于控制嵌套 ThemeProvider 行为
disableNestedContext 若为 true,Provider 会像根 ThemeProvider 一样创建自己的上下文并生成样式表
forceThemeRerender false 为 true 时,模式切换会重新计算主题值,theme.colorSchemes.{mode}.* 节点会被浅合并到主题顶层
noSsr false 为 true 时 ThemeProvider 不重渲染,初始 mode 直接来自本地存储(SSR 需保证服务端输出与客户端首次渲染一致)
disableTransitionOnChange false 切换模式或 color scheme 时禁用 CSS 过渡

源码中还有一处值得注意的分支逻辑:若传入的主题不含 colorSchemes(即非 CSS 变量主题),ThemeProvider 会渲染 ThemeProviderNoVars,并对没有 vars 字段的普通主题强制补上 vars: null,目的是防止嵌套时从上层 CSS 变量主题错误地继承样式变量

主题配置变量

修改主题配置变量是让 Material UI 贴合自身需求的最有效方式。官方文档覆盖的最重要主题变量包括:

  • .palette(颜色调色板)
  • .typography(排版)
  • .spacing(间距)
  • .breakpoints(断点)
  • .zIndex(层叠顺序)
  • .transitions(过渡)
  • .components(组件级定制:defaultProps、styleOverrides 等)

默认主题的全貌可以在仓库文档目录 docs/data/material/customization/ 下的默认主题(default theme)相关页面中查到。

自定义变量(Custom variables)

当你在主题中配合 MUI System 或其他样式方案使用时,往往需要在主题中添加额外的变量以便处处引用:

const theme = createTheme({
  status: {
    danger: orange[500],
  },
});

需要特别警惕的是:vars 是为 CSS 主题变量自动生成的私有字段。如果你在 createTheme 中传入 vars 会直接抛出错误:

createTheme({
  vars: { ... }, // ❌ error
})

这一点在源码中得到印证:createThemeNoVars.js 在检测到 options.varsoptions.generateThemeVars === undefined 时,会抛出 MUI: \vars` is a private field used for CSS variables support.的错误。官方建议自定义对象另起他名(如文档示例中的status`,或警告信息中提示的“use another name”)。

TypeScript 类型增强

ThemeThemeOptions 添加新变量,必须使用 TypeScript 的模块增强(module augmentation):

declare module '@mui/material/styles' {
  interface Theme {
    status: {
      danger: string;
    };
  }
  // allow configuration using `createTheme()`
  interface ThemeOptions {
    status?: {
      danger?: string;
    };
  }
}

完整的可运行示例见 CustomStyles.js。若需要给 theme.palette 添加额外变量,则参见文档中的 palette 定制章节。

颜色操作工具

createTheme 返回的主题对象还会被 attachColorManipulators 附加三个工具方法:theme.alpha(color, coefficient)theme.lighten(color, coefficient)theme.darken(color, coefficient)。从源码结构看,当主题启用了 colorSpace 时,lighten/darken 会使用 color-mix()alpha 会生成 oklch() 表达式;启用 CSS 变量时 alpha 则通过 rgba(var(--xxxChannel) / a) 的形式引用变量通道,保证颜色操作在变量模式下依然动态生效。

主题构建工具(Theme builder)

社区提供了若干可视化的主题构建工具,可以快速设计、预览和编辑主题:

  • mui-theme-creator:帮助为 Material UI 设计并定制主题,附带基础站点模板,展示各组件受主题影响的效果;
  • MUI Theme Builder:生成、预览和编辑 Material UI 主题的工具;
  • Material palette generator:Material Design 官方的调色板生成器,可为任意输入颜色生成一套色阶。

在组件中访问主题:useTheme hook

在函数式组件中,可以用 useTheme hook 访问主题变量:

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

function DeepChild() {
  const theme = useTheme();
  return <span>{`spacing ${theme.spacing}`}</span>;
}

useTheme.js 的实现看,它内部转调了 @mui/systemuseTheme(defaultTheme),并以 defaultTheme 作为兜底默认值——所以即使没有包裹 ThemeProvideruseTheme 返回的也是完整的默认主题而非空对象。返回前它还会先查找内部 THEME_ID 键(theme[THEME_ID] || theme),以支持带内部标记的主题对象。

主题嵌套(Nesting the theme)

可以嵌套多个 ThemeProvider,内层主题会覆盖外层主题。仓库中的示例 ThemeNesting.js 展示了这一行为:外层主题把 primary.main 设为 orange[500],内层主题将其覆盖为 green[500],两个 Checkbox 分别呈现外、内两套主题色。

const outerTheme = createTheme({
  palette: { primary: { main: orange[500] } },
});
const innerTheme = createTheme({
  palette: { primary: { main: green[500] } },
});

export default function ThemeNesting() {
  return (
    <ThemeProvider theme={outerTheme}>
      <Checkbox defaultChecked />
      <ThemeProvider theme={innerTheme}>
        <Checkbox defaultChecked />
      </ThemeProvider>
    </ThemeProvider>
  );
}

如果希望扩展而不是整体替换外层主题,可以给 theme prop 传一个函数。ThemeNestingExtend.js 的完整实现:

const outerTheme = createTheme({
  palette: {
    secondary: {
      main: orange[500],
    },
  },
});

export default function ThemeNestingExtend() {
  return (
    <ThemeProvider theme={outerTheme}>
      <Checkbox defaultChecked color="secondary" />
      <ThemeProvider
        theme={(theme) =>
          createTheme({
            ...theme,
            palette: {
              ...theme.palette,
              primary: {
                main: green[500],
              },
            },
          })
        }
      >
        <Checkbox defaultChecked />
        <Checkbox defaultChecked color="secondary" />
      </ThemeProvider>
    </ThemeProvider>
  );
}

这里内层主题只改写了 primary,外层的 secondary(橙色)仍然保留——两个 checkbox 的 secondary 颜色一致,而内层的 primary 变为绿色。这与 ThemeProviderPropstheme: Partial<Theme> | ((outerTheme: Theme) => Theme) 的类型定义完全对应。

CSS 主题变量(cssVariables)

要基于主题生成 CSS 变量,在主题配置中把 cssVariables 设为 true 并传给 ThemeProvider

const theme = createTheme({
  cssVariables: true,
});

function App() {
  return <ThemeProvider theme={theme}>...</ThemeProvider>;
}

这会生成一份包含 CSS 主题变量的全局样式表:

:root {
  --mui-palette-primary-main: #1976d2;
  /* ...other variables */
}

之后,ThemeProvider 下的所有组件都会使用这些 CSS 主题变量而不是原始值:

- color: #1976d2;
+ color: var(--mui-palette-primary-main);

从源码看,这条路径由 createTheme.tscssVariables !== false 的分支接管,最终交给 createThemeWithVars 生成;而 ThemeProvider.tsx 在检测到主题带有 colorSchemes 时会渲染 CssVarsProvider(来自 ThemeProviderWithVars.tsx),由它负责样式表生成与模式切换。这也解释了为何前文 ThemeProvider 的额外 props(modeStorageKeycolorSchemeNode 等)只在 CSS 变量模式下有意义。

API 详解

createTheme(options, ...args) => theme

根据传入的 options 生成主题,然后把它作为 prop 传给 ThemeProvider

Arguments

  1. options(object):接收一个不完整的主题对象,补齐缺失部分;
  2. ...args(object[]):把后续参数与即将返回的主题做深合并。

注意createTheme() 只处理第一个参数(options)。虽然出于向后兼容目前传多个参数也能工作,但该行为在未来版本可能被移除。为确保代码的前向兼容,建议手动深合并主题对象后以单个对象传入:

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

const theme = createTheme(deepmerge(options1, options2));

Returns

theme(object):完整的、可直接使用的主题对象。

示例

import { createTheme } from '@mui/material/styles';
import { green, purple } from '@mui/material/colors';

const theme = createTheme({
  palette: {
    primary: {
      main: purple[500],
    },
    secondary: {
      main: green[500],
    },
  },
});

createTheme.ts 中可以看到,...args 确实被透传给了 createThemeNoVars/createThemeWithVars,并在内部通过 args.reduce((acc, argument) => deepmerge(acc, argument), muiTheme) 完成深合并——这就是多参数仍然“能工作”的实现来源。

主题组合:用主题选项定义其他选项

当某个主题选项的取值依赖于另一个主题选项时,应分步组合主题:

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

let theme = createTheme({
  palette: {
    primary: {
      main: '#0052cc',
    },
    secondary: {
      main: '#edf2ff',
    },
  },
});

theme = createTheme(theme, {
  palette: {
    info: {
      main: theme.palette.secondary.main,
    },
  },
});

可以把创建主题理解为两步组合过程:先定义基础设计选项,再用这些设计选项组合出其他选项。

警告theme.vars 是用于 CSS 变量支持的私有字段,自定义对象请使用其他名称。

defaultProps 中 className 与 style 的合并(mergeClassNameAndStyle)

默认情况下,当组件在主题中定义了 defaultProps 时,直接传给组件的 props 会完全覆盖默认 props:

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

const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: {
        className: 'default-button-class',
        style: { marginTop: 8 },
      },
    },
  },
});

// className will be: "custom-button-class" (default ignored)
// style will be: { color: 'blue' } (default ignored)
<Button className="custom-button-class" style={{ color: 'blue' }}>
  Click me
</Button>;

你可以把 theme.components.mergeClassNameAndStyle 配置为 true,让 classNamestyle 走“合并”而非“替换”:

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

const theme = createTheme({
  components: {
    mergeClassNameAndStyle: true,
    MuiButton: {
      defaultProps: {
        className: 'default-button-class',
        style: { marginTop: 8 },
      },
    },
  },
});

该配置下的效果:

// className will be: "default-button-class custom-button-class"
// style will be: { marginTop: 8, color: 'blue' }
<Button className="custom-button-class" style={{ color: 'blue' }}>
  Click me
</Button>

从源码结构看,该选项的类型声明位于 components.tscomponents 选项的 mergeClassNameAndStyle?: boolean 字段,行为验证见 createTheme.spec.ts

responsiveFontSizes(theme, options) => theme

根据传入的 options 生成响应式排版设置。

Arguments

  1. theme(object):要增强的主题对象;
  2. options(object,可选):
    • breakpoints(array<string>,可选):默认 ['sm', 'md', 'lg']。要处理的断点标识符数组;
    • disableAlign(bool,可选):默认 false。为 true 时字号变化幅度会略作调整,使行高保持在 Material Design 的 4px 行高网格上对齐;要求主题样式中使用无单位的行高;
    • factor(number,可选):默认 2。决定字号缩放强度:值越大,小屏上各字号差异越小;值越小,小屏字号越大。值必须大于 1;
    • variants(array<string>,可选):默认处理全部排版变体。要处理的 typography 变体列表。

Returns

theme(object):带响应式排版的新主题。

示例

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

let theme = createTheme();
theme = responsiveFontSizes(theme);

responsiveFontSizes.js 的实现细节与文档参数完全一致:默认 variants 列表包含 h1~h6subtitle1/2body1/2captionbuttonoverline;字号按 minFontSize = 1 + (maxFontSize - 1) / factor 公式在小屏收敛到 1rem 基准;当行高非无单位且未设 disableAlign 时会抛出 Unsupported non-unitless line height with grid alignment 错误;并且实现中保留了“最大断点处字号回到原始设计值”的修正逻辑(对应上游 issue #40255)。

enhanceHighContrast(theme, tokens) => theme

为应用 @media (forced-colors: active) 覆盖规则的主题做增强,提升组件在 Windows 高对比度 / Forced Colors 模式下的可见性。自 v9.1.0 起可用。它接收一个已创建完成的主题并返回增强版本。

Arguments

  1. theme(object):要增强的主题对象;
  2. tokens(object,可选):用于覆盖个别默认值的 CSS 系统颜色关键字对象。
Token 默认值 说明
disabled GrayText 禁用元素颜色
error ActiveText 错误状态颜色
selectedBackground SelectedItem 选中项背景色
selectedText SelectedItemText 选中项上的文字颜色
activeBackground Highlight 激活/切换控件的背景色
activeText HighlightText 激活/切换控件上的文字颜色
buttonBorder ButtonBorder 交互控件的边框颜色
buttonText ButtonText 按钮上的文字/图标颜色
canvas Canvas 页面/画布背景色

Returns

theme(object):新主题,在受影响组件上应用了 forced-colors 覆盖。

示例

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

// Use defaults
let theme = createTheme();
theme = enhanceHighContrast(theme);
// Override individual tokens
let theme = createTheme();
theme = enhanceHighContrast(theme, {
  activeBackground: 'SelectedItem',
  activeText: 'SelectedItemText',
});

enhanceHighContrast.ts 的实现比文档描述的更具体:它针对 MuiCheckboxMuiRadioMuiSwitchMuiSliderMuiMenuItemMuiListItemButtonMuiButtonBaseMuiAutocompleteMuiTooltipMuiToggleButton、各类 Input 等约 20 个组件注入 @media (forced-colors: active)styleOverrides。值得学习的实现手法有两点:一是使用数组形式styleOverrides 条目(如 root: [existing, newRule]),让 Emotion 把每个条目输出为独立 CSS 规则、交由浏览器级联解决优先级,避免直接覆盖用户已有的覆盖;二是对 Checkbox/Radio/Switch/Slider 这类“焦点或禁用元素是隐藏内部 input”的组件,用 :focus-within:has(input:focus-visible) 选择器恢复焦点轮廓,并借助 ownerState.disabled 处理不接收禁用类的 track/thumb 槽位。

unstable_createMuiStrictModeTheme(options, ...args) => theme

警告:不要在生产环境使用此方法。

生成一个减少 React.StrictMode 内警告(例如 Warning: findDOMNode is deprecated in StrictMode)的主题。

Requirements

目前 unstable_createMuiStrictModeTheme 不增加额外要求。

Arguments

  1. options(object):接收不完整的主题对象并补齐缺失部分;
  2. ...args(object[]):把参数与即将返回的主题深合并。

Returns

theme(object):完整的、可直接使用的主题对象。

示例

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

const theme = unstable_createMuiStrictModeTheme();

function App() {
  return (
    <React.StrictMode>
      <ThemeProvider theme={theme}>
        <LandingPage />
      </ThemeProvider>
    </React.StrictMode>
  );
}

其实现位于 createMuiStrictModeTheme.js

小结与实践建议

  • 主题入口是 createTheme,注入靠 ThemeProvider,读取用 useTheme,三者分别对应仓库中的 createTheme.tsThemeProvider.tsxuseTheme.js
  • 传多参数给 createTheme 属于待移除的兼容行为,新代码应使用 deepmerge 手工合并后传单对象;
  • vars 是保留字段,自定义变量请另起命名并用 TypeScript 模块增强补齐类型;
  • 嵌套 ThemeProvider 时,传函数形式 theme 可以基于外层主题做增量修改,避免内层主题把外层配置整体覆盖掉;
  • 需要运行时切换 light/dark 且希望切换即时生效时,优先考虑 cssVariables: true 路径;
  • 生产环境请避免使用 unstable_ 前缀的 API;涉及高对比度无障碍需求时,可在 v9.1.0+ 中用 enhanceHighContrast 一步完成 forced-colors 适配。

更多配套文档可参考官方 Theming 文档源文件 theming.md,以及文档数据目录下的 ThemeNesting.jsThemeNestingExtend.js 两个可直接运行的示例。

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