Material UI System Shadows 阴影指南:用 boxShadow 工具函数实现 25 级 Z 轴高程
阴影(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 |
它说明三件关键事实:
- Style function:
shadows是从@mui/system导出的一个 System style function; - Prop 名:使用它时书写的 prop 是
boxShadow; - 主题映射:
boxShadow值会被解析到主题的shadowskey,最终输出原生 CSS 属性box-shadow。
源码拆解:从 boxShadow 到 box-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 集合里,shadows 与 borders、palette、spacing 等一起通过 compose 合入 SimpleSystemKeys(见 [packages/mui-system/src/Box/Box.tsx#L31-L46])。因此无论是 sx 还是 styled()/Box 的直接 prop 写法,都能解析 boxShadow。getThemeValue 中的 propToStyleFunction 也注册了 boxShadow → shadows 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-web 的 mdc-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),自定义阴影表时不要破坏这一约定。 - 支持响应式与函数式 sx:
boxShadow可以接收断点对象,也可以在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 中像使用间距与断点一样,系统性地管理元素的纵深关系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00