首页
/ Material UI System Shadows 阴影指南:用 boxShadow 工具函数实现 25 级 Z 轴高程

Material UI System Shadows 阴影指南:用 boxShadow 工具函数实现 25 级 Z 轴高程

2026-09-06 18:24:58作者:薛曦旖Francesca

阴影(Shadows)是 Material Design 视觉语言中最能体现"层级关系"的元素之一。Material UI 在 System 层提供了 boxShadow 工具函数,允许你通过 sx 或独立 style function 在任意元素上按主题统一添加、移除阴影,从而表达两个表面之间沿 Z 轴(纵深)的相对距离。本文基于仓库中的官方文档 docs/data/system/shadows/shadows.md 展开,结合 @mui/system@mui/material 的源码,讲清 25 级默认高程从何而来、如何映射到 CSS box-shadow、如何覆盖主题,以及 Paper 等组件内部如何使用它。读完你将能熟练地在 Box、自研组件与整站主题中精确控制表面高程。

阴影工具解决什么问题

在 Material Design 体系中,阴影的作用不是装饰,而是表达深度信息:当两个表面沿 z 轴方向一高一低时,离观察者更近的表面对更远的表面投下阴影。阴影越"重",两者在纵深上的距离就越远。

System 的 Shadows 工具函数正是为了规范化这套深度体系而存在。通过 docs/data/system/shadows/shadows.md 中的一句话即可概括其用法:

这些 helper 允许你控制两个表面之间沿 z 轴的相对深度(或距离)。默认提供 25 级高程。

它把 "深 / 浅" 这种模糊描述,抽象成可直接书写在 JSX 里的数值索引,并由主题统一决定每个索引对应的真实 box-shadow 值。

高程与 25 级默认值

"25 级高程"对应 theme.shadows 数组中下标 0 到 24 的 25 个条目。在 ShadowsDemo.js(及其 TypeScript 版本 ShadowsDemo.tsx)这个官方演示里,四个 Box 依次使用 0、1、2、3 号阴影来展示递进效果,而每个 Box 都用到了完整的 Surface 形态:白/灰底色、圆角、内边距与居中文字,让你能直观看到不同 boxShadow 强度在真实卡片上的差异。

快速上手:三种等效写法

官方文档给出的示例是以数字形式书写 boxShadow,由主题负责解析成真实的 CSS 阴影字符串:

<Box sx={{ boxShadow: 0 }}>…   {/* 无阴影('none') */}
<Box sx={{ boxShadow: 1 }}>…   {/* 1 号阴影 */}
<Box sx={{ boxShadow: 2 }}>…   {/* 2 号阴影 */}
<Box sx={{ boxShadow: 3 }}>…   {/* 3 号阴影 */}

注意这里 0 的语义是 theme.shadows[0]。在默认主题中该值就是字符串 'none',因此 boxShadow: 0 等价于移除阴影,这也是文档标题里 "Add or remove shadows" 中 "remove" 的含义。

该 demo 的实际实现(见 ShadowsDemo.tsx)展示了一个更完整的真实用法——使用函数形式的 sx 以访问 theme,并通过 theme.applyStyles('dark', {...}) 为暗色模式单独覆盖底色与文字色,从而保证阴影卡片在明暗两种主题下都清晰可辨:

import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';

<Box
  sx={(theme) => ({
    boxShadow: 0,
    width: '8rem',
    height: '5rem',
    bgcolor: '#fff',
    color: 'grey.800',
    p: 1,
    m: 1,
    borderRadius: 2,
    textAlign: 'center',
    fontSize: '0.875rem',
    fontWeight: '700',
    ...theme.applyStyles('dark', {
      bgcolor: '#101010',
      color: 'grey.300',
    }),
  })}
>
  boxShadow: 0
</Box>

官方 API 一览

文档的 API 段落(shadows.md)给出了完整的映射关系:

import { shadows } from '@mui/system';
Import name Prop CSS property Theme key
boxShadow boxShadow box-shadow shadows

它说明三件关键事实:

  1. Style functionshadows 是从 @mui/system 导出的一个 System style function;
  2. Prop 名:使用它时书写的 prop 是 boxShadow
  3. 主题映射boxShadow 值会被解析到主题的 shadows key,最终输出原生 CSS 属性 box-shadow

源码拆解:从 boxShadowbox-shadow 的完整链路

① System 层的 style function

@mui/system 中,阴影功能是一个非常薄的定义,见 packages/mui-system/src/shadows/shadows.ts

import style from '../style';

const boxShadow = style({
  prop: 'boxShadow',
  themeKey: 'shadows',
});

它调用了 style() 工厂函数:当检测到 boxShadow prop 存在时,先从主题中取出 theme.shadows,再解析值并输出 { boxShadow: <最终值> }cssProperty 缺省时等于 prop 名,即 box-shadow)。shadows 随后通过 packages/mui-system/src/index.js 导出给外界使用。

② 数值索引如何变成 CSS 字符串

很多人会好奇:boxShadow: 3 到底是怎么变成一大段 rgba(...) 字符串的?答案在 getStyleValue2(见 style.ts)的数组分支逻辑中:

} else if (Array.isArray(themeMapping)) {
  value = themeMapping[userValue as any] || userValue;
}

由于 theme.shadows 恰好是一个数组(详见下一节),数字 3 会作为数组下标取出 theme.shadows[3];若给定的下标取不到值,则回退为原始输入。换句话说,数值与字符串两种形式都受支持:

<Box sx={{ boxShadow: 3 }} />                 {/* 解析为 theme.shadows[3] */}
<Box sx={{ boxShadow: 'none' }} />            {/* 未命中数组时原样输出 */}
<Box sx={{ boxShadow: '0 0 8px #000' }} />    {/* 完全自定义的裸 CSS 值 */}

同时,handleBreakpoints 的接入使 boxShadow 天然支持响应式对象:

<Box
  sx={{
    boxShadow: {
      xs: 1,  // 移动端:浅阴影
      md: 8,  // ≥ md 断点:加深
    },
  }}
/>

③ sx 系统里的注册

boxShadow 之所以能在 sx={{ boxShadow: ... }} 中开箱即用,是因为它被登记在 defaultSxConfig.ts

// shadows
boxShadow: { themeKey: 'shadows' },

而在 Box 的系统 prop 集合里,shadowsborderspalettespacing 等一起通过 compose 合入 SimpleSystemKeys(见 [packages/mui-system/src/Box/Box.tsx#L31-L46])。因此无论是 sx 还是 styled()/Box 的直接 prop 写法,都能解析 boxShadowgetThemeValue 中的 propToStyleFunction 也注册了 boxShadowshadows style function 的映射(见 getThemeValue.ts)。

25 级高程的真实样貌:默认主题阴影表

boxShadow 只是"钥匙",真正的阴影值存放在 theme.shadows 中。Material UI 的默认实现位于 packages/mui-material/src/styles/shadows.js,它的开头声明了三个 Material Design 阴影规范的关键透明度系数:

const shadowKeyUmbraOpacity = 0.2;        // 阴影核心(umbra)透明度
const shadowKeyPenumbraOpacity = 0.14;    // 阴影外围(penumbra)透明度
const shadowAmbientShadowOpacity = 0.12;  // 环境光阴影(ambient)透明度

随后,文件用 createShadow(...px) 工厂函数生成每条阴影由三层叠加组成的完整值:

function createShadow(...px) {
  return [
    `${px[0]}px ${px[1]}px ${px[2]}px ${px[3]}px rgba(0,0,0,${shadowKeyUmbraOpacity})`,
    `${px[4]}px ${px[5]}px ${px[6]}px ${px[7]}px rgba(0,0,0,${shadowKeyPenumbraOpacity})`,
    `${px[8]}px ${px[9]}px ${px[10]}px ${px[11]}px rgba(0,0,0,${shadowAmbientShadowOpacity})`,
  ].join(',');
}

即每一条 box-shadow 都是 umbra + penumbra + ambient 三层半透明黑色阴影的叠加,这正是 Material Design 阴影有"柔和层次"而不是生硬单条黑影的原因。文件中还注明这些数值参考自 material-components-webmdc-elevation/_variables.scss。整个数组以 'none' 开头,下标 0~24 共 25 项(见同目录 shadows.d.ts,其类型 Shadows 是一个以 'none' 为首项、长度 25 的元组)。

一份可查证的默认值样本

createTheme.test.js(以及 extendTheme.test.js)断言了默认主题中 shadows[2] 的确切值:

expect(theme.shadows[2]).to.equal(
  '0px 3px 1px -2px rgba(0,0,0,0.2),' +
  '0px 2px 2px 0px rgba(0,0,0,0.14),' +
  '0px 1px 5px 0px rgba(0,0,0,0.12)',
);

可以看到它正是三层阴影以逗号拼接的结果,透明度分别是 0.2 / 0.14 / 0.12,与源码中的三个常量一一对应。这个测试用例可以作为你自行核对默认阴影表的权威参照。

阴影表如何进入主题

createTheme() 执行时,默认阴影数组会被拷贝进主题对象。注意源码特意注释了不能直接 [...shadows](涉及转译后迭代器协议的坑),而是使用 shadows.slice()(见 createThemeNoVars.js)。

muiTheme = deepmerge(systemTheme, {
  ...
  shadows: shadows.slice(),
  ...
});

覆盖主题:定制你自己的高程体系

由于 boxShadow 只是读取 theme.shadows定制全局阴影最优雅的方式是替换主题中的整个数组,而不是在每一处写死 CSS 值。同样在 createTheme.test.js 中给出了可运行的覆盖示例:传入一个长度 25 的数组(首项通常为 'none'),甚至允许数字类型的中间项(说明阴影表不强制要求都是字符串):

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

const theme = createTheme({
  shadows: [
    'none',
    1, 1, 1, 2, 3, 3, 4, 5, 5, 6,
    6, 7, 7, 7, 8, 8, 8, 9, 9, 10,
    10, 10, 11, 11, 11,  // 长度需保持为 25
  ],
});

重要约束:自定义数组必须保持 25 项(下标 0~24)。组件内部通常会直接按下标取值,若某项缺失就会得到 undefined,从而产生非预期效果。

组件侧的下标消费:以 Paper 为例

阴影表并不是只服务于 Box。Material UI 中带"高程"语义的组件(如 Paper)会把 elevation prop 翻译成对阴影数组的下标访问。在 packages/mui-material/src/Paper/Paper.js 中可以看到它先做健壮性校验:

if (theme.shadows[elevation] === undefined) {
  throw new Error(
    `Please make sure that \`theme.shadows[${elevation}]\` is defined.`,
  );
}
...
'--Paper-shadow': (theme.vars || theme).shadows[elevation],

也就是说,<Paper elevation={8} /> 最终渲染出的阴影其实就是 theme.shadows[8]。因此当你通过主题统一调整阴影表时,Box、Paper 以及一切依赖高程的组件会同步变化,这正是把阴影抽象进主题的意义所在。类似的引用也出现在 Breadcrumbs 内部折叠按钮的还原样式(theme.shadows[0])等位置。

实践建议与注意事项

  • 用数值表达层级,用主题统一风格:团队协作时建议只使用 0~24 的数值索引,让设计师在主题中一次性定义"多深算一级",避免散落的魔法字符串。
  • boxShadow: 0 是移除阴影:它解析到 theme.shadows[0] = 'none',可用于 Material 组件默认带阴影时的去阴影场景。
  • 首项务必是 'none':Material 组件在很多地方依赖 shadows[0] 表示"无阴影"(如前述 Breadcrumbs),自定义阴影表时不要破坏这一约定。
  • 支持响应式与函数式 sxboxShadow 可以接收断点对象,也可以在 sx 函数中基于 theme 计算,配合 theme.applyStyles 可以做出明暗模式差异化的高程表现。
  • 三层叠加是设计使然:若你觉得默认阴影"太黑/太软",优先通过覆盖阴影表整体调整,而不是只覆盖单个值,否则容易破坏层级间的连贯递增关系。
  • 组件直接下标消费:自行写组件时,若需要"表意深度",请读取 theme.shadows[level] 而非硬编码 CSS,这样你的组件也能跟随用户的阴影主题自动适配。

小结

Material UI System 的 Shadows 工具把 CSS 中繁琐、难以记忆的 box-shadow 多层字符串,收敛为一个 boxShadow prop + 25 级高程索引 + 可整体覆盖的 theme.shadows 数组。无论你是想在 Box 上快速加一层阴影、让 Paper 浮起来,还是为整个应用建立统一且可复用的"深度语言",这套机制都提供了从 style function 定义sx 配置默认阴影表主题注入组件消费 的完整闭环。掌握它,你就能在 Material UI 中像使用间距与断点一样,系统性地管理元素的纵深关系。

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