Material Components Web 形状系统(Shape)完全指南:圆角体系、Sass 变量与 radius 混入深度解析
【免费下载链接】material-components-webModular and customizable Material Design UI components for the web项目地址: https://gitcode.com/gh_mirrors/ma/material-components-web
本文以
@material/shape包(Material Components for the web 的形状系统)为核心,系统讲解其设计意图、安装方式、Sass 变量与 CSS 自定义属性、四个核心 Sass 函数以及radius()混入的使用方法与底层实现原理,并结合固定高度、动态高度、指定角落与组件主题化四类实战场景,帮助读者在按钮、卡片、抽屉等组件上精准应用圆角形状。
什么是 Material 形状系统
在 Material Design 的设计语言中,形状(Shape)用于引导注意力、标识组件、传达状态并表达品牌个性。@material/shape是 material-components-web 中独立封装形状工具的包,它为所有其他组件提供统一的圆角(radius)处理能力,是按钮、卡片、抽屉、输入框等组件主题化的基础设施之一。
该包当前的唯一支持形状是圆角(rounded corners),README 与源码均明确标注了这一点(见 packages/mdc-shape/README.md),源码中resolve-radius()对非rounded的 family 会直接抛出错误:
@error 'mdc-shape: Invalid shape family: "#{$family}". Only "rounded" is supported.';因此本文所有讨论都围绕圆角展开,不涉及剪角(cut corners)、凹陷(concave)等其他形状家族。
安装与引入
作为独立 npm 包,安装命令为:
npm install @material/shape在 package.json 中可以看到,该包版本为 14.0.0(与仓库其他包保持一致),声明了 MIT 许可,并依赖以下模块:
@material/feature-targeting:特性定位(编译期裁剪未启用特性的样式)@material/rtl:RTL(从右到左)语言环境下的样式翻转@material/theme:主题变量与 CSS 自定义属性输出tslib:TypeScript 运行时辅助库
在 SCSS 中引入整个形状模块:
@use "@material/shape";模块入口 packages/mdc-shape/_index.scss 分别转发了variables、mixins与functions三个子模块,因此一次@use即可同时获得变量、混入与函数。
兼容性说明:仓库同时保留了
_mixins.scss、_functions.scss、_variables.scss三个文件,它们仅做@forward './shape'转发,并在注释中标注@deprecated,建议新代码直接使用_shape.scss模块(即@use "@material/shape")。
形状类别与 Sass 变量
Material 形状系统把组件划分为small(小)、medium(中)、large(大)三个类别。覆盖下面的 Sass 变量,会一次性改变对应类别下所有组件的圆角。
| 变量 | 描述 | 默认值 |
|---|---|---|
$small-component-radius | 小尺寸组件的圆角半径 | 4px |
$medium-component-radius | 中尺寸组件的圆角半径 | 4px |
$large-component-radius | 大尺寸组件的圆角半径 | 0 |
这些变量的定义位于 packages/mdc-shape/_shape.scss 顶部:
// Shape categories $small-component-radius: 4px !default; $medium-component-radius: 4px !default; $large-component-radius: 0 !default;变量使用!default声明,意味着只有当调用方尚未定义同名变量时才会生效,这保证了使用者可以通过 Sass 变量覆盖机制安全地全局定制形状。定义之后,这三个类别值还会被注册为主题键(theme keys),以便与主题系统统一协作:
@include keys.set-values( ( small: $small-component-radius, medium: $medium-component-radius, large: $large-component-radius, ), $options: (custom-property-prefix: shape) );此外,源码还提供两个辅助函数用于操作类别键:
is-shape-key($radius):判断传入值是否为small/medium/large之一;get-shape-keys():返回全部形状类别键的列表。
组件属于哪个类别,可参考 Material Design 官方 Shape 指南对组件的归类说明;类别选择会直接影响最终渲染出来的圆角观感。
CSS 自定义属性
除了 Sass 变量,形状系统还对外暴露一组 CSS 自定义属性,方便在运行时(例如通过 JavaScript 或内联样式)动态调整圆角。
| CSS 自定义属性 | 描述 | 默认值 |
|---|---|---|
--mdc-shape-small | 小尺寸组件的圆角半径 | 4px |
--mdc-shape-medium | 中尺寸组件的圆角半径 | 4px |
--mdc-shape-large | 大尺寸组件的圆角半径 | 0 |
这组自定义属性正是上文keys.set-values以shape为前缀注册的产物,small、medium、large分别对应--mdc-shape-small、--mdc-shape-medium、--mdc-shape-large。
⚠️重要限制:不要在自定义属性中使用百分比值。因为
shape.radius()在运行时无法解析百分比对应的组件高度,百分比的解析必须依赖编译期传入的$component-height。源码中百分比到绝对值的转换发生在 Sass 编译阶段(见下文resolve-radius()的_resolve-radius-percentage内部函数),CSS 自定义属性在运行时并不能完成这一换算。
Sass 函数详解
形状模块提供了四个公开 Sass 函数,它们分别负责半径值的解析、翻转、掩码与展开。下面结合 packages/mdc-shape/_shape.scss 的源码逐一说明。
resolve-radius($radius, $component-height)
返回某个形状类别(large、medium、small)解析后的半径值。如果传入的不是类别名,则在该值合法(数字或百分比)时原样返回。
$component-height在$radius可能为百分比时必须提供,用于把百分比换算为绝对像素值。
源码中的核心分支逻辑如下(简化说明):
- 传入
null:直接返回null; - 传入列表:递归解析每个角落值,列表长度必须为 1~4,否则报错
"Radius must be between 1 and 4 values."; - 传入形状类别键:先通过
keys.create-custom-property($radius)转为自定义属性再递归解析; - 传入自定义属性 Map:解析其 fallback 值并重新写回;
- 传入形状 Map(含
family与radius字段):校验family必须为rounded,随后解析radius字段; - 其余情况视为数值:若单位是
%且提供了$component-height,则调用内部函数换算为绝对值:
@function _resolve-radius-percentage($percentage, $component-height) { // 50% => 50,再乘以高度百分比 $percentage: math.div($percentage, $percentage * 0 + 1); @return $component-height * math.div($percentage, 100); }例如resolve-radius(50%, $component-height: 36px)的结果就是16px。
flip-radius($radius)
在 RTL(从右到左)语境下翻转半径值。$radius为 2~4 个角落值的列表,翻转规则等价于把水平方向上的两对角互换:
- 4 值:
(a b c d)→(b a d c); - 3 值:
(a b c)→(b a b c); - 2 值:
(a b)→(b a); - 单值:原样返回。
若列表超过 4 个值,函数会直接报错:"Invalid radius: ... is more than 4 values"。
mask-radius($radius, $masked-corners)
接受一个半径数字或 2~4 个值的列表,返回一个 4 值列表,其中未被掩码($masked-corners中对应位为 0)的角落被置为 0,保留掩码位为 1 的角落值。$masked-corners必须是长度为 4 的列表,否则报错。
源码示例注释给出了三个直观用例:
// mask-radius(2px 3px, 1 1 0 0) => 2px 3px 0 0 // mask-radius(8px, 0 0 1 1) => 0 0 8px 8px // mask-radius(4px 4px 4px 4px, 0 1 1 0) => 0 4px 4px 0该函数在实现上先调用unpack-radius展开为 4 值,再按掩码逐位决定保留或清零。
unpack-radius($radius)
展开border-radius的简写值(1~3 个值的列表);如果传入的是 4 值列表,则原样返回。它内部直接复用了主题模块的css.unpack-value():
@function unpack-radius($radius) { @return css.unpack-value($radius); }展开规则与 CSSborder-radius简写一致:
// unpack-radius(4px) => 4px 4px 4px 4px // unpack-radius(4px 2px) => 4px 2px 4px 2px // unpack-radius(4px 2px 2px) => 4px 2px 2px 2px // unpack-radius(4px 2px 0 2px)=> 4px 2px 0 2pxradius() 混入:核心 API 与源码原理
radius($radius, $rtl-reflexive)是形状模块最核心的混入,所有其他组件都通过它把圆角应用到对应角落。
| 参数 | 描述 |
|---|---|
$radius | 单个值或最多 4 个角落值的列表 |
$rtl-reflexive | 设为true时在 RTL 语境下翻转半径,默认false |
混入的完整签名在源码中还支持$component-height与$query两个可选参数(packages/mdc-shape/_shape.scss 的radius混入),其中$query用于特性定位裁剪。
其核心执行流程可以拆解为四步:
- 计算是否需要翻转:仅当
$rtl-reflexive为 true 且$radius是多个值的列表时(即$has-multiple-corners为 true),才认为需要翻转——注释明确写道"即使开启了$rtl-reflexive,只要半径明显是对称的,就不输出 RTL 样式"; - 解析半径:调用
resolve-radius()把类别名、百分比等统一解析为最终值; - 单值场景:
$radius不是多角列表时,直接输出一条border-radius属性(通过theme.property支持自定义属性输出); - 多值场景:
unpack-radius展开为 4 值后,分别输出border-top-left-radius、border-top-right-radius、border-bottom-right-radius、border-bottom-left-radius四条属性;若需要翻转,再在rtl.rtl包裹块中用flip-radius()输出翻转后的四角值。
从源码结构看,radius混入借助@material/theme的theme.property输出属性,这意味着当传入自定义属性 Map 时,生成结果仍能保留 CSS 自定义属性的引用,从而与--mdc-shape-*体系无缝衔接。
四类实战场景
1. 固定高度组件(如按钮)
固定高度组件(如标准按钮)需要把百分比圆角换算成绝对值,因此必须传入组件高度:
@use "@material/button"; @use "@material/shape"; @include shape.radius($radius, $component-height: button.$height);其中button.$height是标准按钮的高度,$radius是形状大小。shape.radius()会基于组件高度把百分比单位解析为绝对半径值。按钮组件内部正是这样工作的:在 packages/mdc-button/_button-shared-theme.scss 中,shape-radius()混入通过_shape-radius-with-height()最终调用shape.radius(),并默认使用$density-default-scale对应的高度。
2. 动态高度组件(如卡片)
动态高度组件(如卡片)无法预知高度,因此$radius只能是绝对值:
@include shape.radius($radius);这里的$radius只能传绝对长度(如8px),不能传百分比——因为缺少固定高度作为换算基准。
3. 指定特定角落(如抽屉)
只给部分角落应用圆角时,传入 1~4 值的列表并开启 RTL 反射。抽屉只在右侧显示圆角的经典写法:
@include shape.radius(0 $radius $radius 0, $rtl-reflexive: true);该写法只定制右上角与右下角;当页面处于 RTL 语境时,shape.radius()会自动把半径翻转到左侧,保证镜像布局下视觉对称。
4. 组件级主题化(以按钮为例)
实际开发中最常见的是通过各组件的专属混入间接使用形状系统。以按钮为例,给按钮应用 50% 药丸形状:
@use "@material/button"; .my-custom-button { @include button.shape-radius(50%); }这里的 50% 会被按钮组件的内部实现结合按钮高度解析为半高半径,从而形成药丸造型;也可以传入绝对值(如8px)。
使用建议:Shape API 通常不直接调用,而是经由各组件自己的混入间接使用——组件混入会负责设置高度、并把圆角应用到该组件所有适用变体的正确角落,使用者只需关注传入的半径值即可。
源码与测试导览
- 核心实现:packages/mdc-shape/_shape.scss 包含全部变量、函数与混入;packages/mdc-shape/_index.scss 是模块入口。
- 兼容转发层:packages/mdc-shape/_mixins.scss、packages/mdc-shape/_functions.scss、packages/mdc-shape/_variables.scss 均为已标记 deprecated 的转发文件,另有对应的
*.import.scss旧式引入入口。 - 单元测试:packages/mdc-shape/test/shape.test.scss 使用
true断言框架,重点覆盖resolver($shape)函数:验证null时四角全为null、单值展开到全部角落、1/2/3/4 值列表分别映射到start-start、start-end、end-end、end-start逻辑角落的映射规则。 - 特性定位测试:packages/mdc-shape/test/feature-targeting-any.test.scss 与 packages/mdc-shape/test/mdc-shape.scss.test.ts 共同验证:当查询条件为
feature-targeting.any()时不输出任何 CSS,从而保证样式可按需裁剪。
总结与注意事项
- 当前形状系统仅支持圆角,传入其他 shape family 会在编译期报错;
- 三个类别(small / medium / large)通过 Sass 变量与 CSS 自定义属性双通道可调,默认分别为
4px、4px、0; - CSS 自定义属性禁止使用百分比,百分比解析依赖编译期传入的
$component-height; - 优先通过组件自身的形状混入(如
button.shape-radius())使用形状系统,而不是直接调用shape.radius(); $rtl-reflexive仅在多角值列表且确实不对称时才产生 RTL 翻转样式,对称半径不会产生冗余输出。
【免费下载链接】material-components-webModular and customizable Material Design UI components for the web项目地址: https://gitcode.com/gh_mirrors/ma/material-components-web
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考