上一篇跑通了 mat.theme 与明暗切换,本篇回答更硬核的问题:设计稿给的是一组品牌色与中性色,怎么精确落到 Material 组件上?按钮高度想改怎么办?light/dark 两套定制值怎么在 Sass 里组织不失控?答案都指向同一个机制——token 覆盖。M3 主题定制的全部工作,本质上就是”覆盖哪些层级的变量、在哪里覆盖”。
先想清楚:覆盖哪一层
回顾三层的职责,定制动作按”改的东西离设计稿多近”选择抓手:
| 你想改的 | 抓手 | 影响面 |
|---|---|---|
| 整体配色气质 | mat.theme 换 palette | 全部组件 |
| 具体颜色值(品牌精确色值) | mat.theme-overrides 覆盖 system tokens | 消费该 token 的所有组件 |
| 某个组件的尺寸/形状 | CSS 覆盖 component tokens | 单个组件实例或全部实例 |
| 局部区域换个强调色 | 作用域内重定义 system tokens | 子树 |
原则:优先高层。能用 palette 解决的不写 overrides,能用 system token 解决的不碰 component token——层级越低,升级 Material 时的维护成本越高。
mat.theme-overrides:精修 system tokens
mat.theme 从 palette 生成的 token 值未必与品牌规范逐字节一致。mat.theme-overrides 允许按名字覆盖任意 system token:
@use '@angular/material' as mat;
html {
@include mat.theme((
color: (
primary: mat.$violet-palette,
tertiary: mat.$orange-palette,
),
));
// 品牌精修:只动需要的几个 token
@include mat.theme-overrides((
primary: #5b3cc4, // 主色
on-primary: #ffffff, // 主色上的内容色
primary-container: #e7deff, // 主色容器(浅底色块)
on-primary-container: #1d005f, // 容器上的内容色
surface: #fdf8ff, // 页面底色
on-surface: #1d1b20, // 正文色
));
}
key 就是去掉 --mat-sys- 前缀的 token 名。M3 颜色 token 都是成组出现的(X 与 on-X),覆盖时成组覆盖——只改 primary 不改 on-primary,高对比场景下可能出现”深底深字”。
用 sass:map 管理覆盖表
覆盖项一多,直接写在 mixin 调用里会失控。用 sass:map 把品牌规范做成可组合的数据:
// styles/_tokens.brand.scss
$brand: (
primary: #5b3cc4,
on-primary: #ffffff,
primary-container: #e7deff,
on-primary-container: #1d005f,
);
// styles/_tokens.light.scss
@use 'sass:map';
@use './tokens.brand';
$light: map.merge(tokens.brand.$brand, (
surface: #fdf8ff,
on-surface: #1d1b20,
surface-container: #f3edf7,
outline: #79747e,
));
// styles/_tokens.dark.scss
@use 'sass:map';
@use './tokens.brand';
$dark: map.merge(tokens.brand.$brand, (
surface: #141218,
on-surface: #e6e0e9,
surface-container: #211f26,
outline: #938f99,
));
品牌色是两个主题共享的”不变量”,明暗各自的底色、边框是”变量”——map.merge 的组合正好表达了这个结构。主题入口变得极短:
// styles.scss
@use '@angular/material' as mat;
@use './styles/tokens.light' as light;
@use './styles/tokens.dark' as dark;
html {
color-scheme: light dark;
@include mat.theme((
color: (
primary: mat.$violet-palette,
tertiary: mat.$orange-palette,
),
));
}
// 浅色定制:跟随系统或手动选择
@media (prefers-color-scheme: light) {
html:not([data-theme]) {
@include mat.theme-overrides(light.$light);
}
}
html[data-theme='light'] {
@include mat.theme-overrides(light.$light);
}
// 深色定制
@media (prefers-color-scheme: dark) {
html:not([data-theme]) {
@include mat.theme-overrides(dark.$dark);
}
}
html[data-theme='dark'] {
@include mat.theme-overrides(dark.$dark);
}
这四个块覆盖了三态开关(系统浅 / 系统深 / 手动)的全部组合,data-theme 的设置逻辑沿用第 36 篇的 ThemeService,前端行为零新增。
从品牌色生成整套 palette
mat.theme 只能从预置 palette 里选,不能直接”给一个 seed 色生成 tonal palette”——M3 的 tonal 算法发生在 Sass 之外。当设计稿给出的是完整的设计 token(而非单一品牌色)时,官方推荐的工作流是:设计侧用 Material Theme Builder 从 seed 色导出整套 token 清单,前端把导出的值整理成上面的 overrides map。也就是说:palette 选择交给工具,token 落地交给 theme-overrides。
组件级 token:动组件的”体检指标”
每个 Material 组件还暴露一组组件变量,控制高度、圆角、内边距等细节。它们以 --mat-<组件名>-* 命名,默认值引用 system tokens。覆盖方式就是普通 CSS 变量赋值:
// 全部填充按钮:40px 高、8px 圆角
html {
--mat-button-filled-container-height: 40px;
--mat-button-filled-container-shape: 8px;
}
// 只影响某个区域内的表单控件
.density-panel {
--mat-form-field-filled-container-height: 48px;
}
判断”我要改的东西是不是组件 token”的方法:打开该组件文档的 Theming/Styling 小节,官方会列出全部变量名。实践中两个高频用法:
// 用法一:卡片圆角统一为 12px(设计规范值)
.app-card {
--mat-card-elevated-container-shape: 12px;
}
// 用法二:让工具栏使用品牌渐变之外的实色
.app-header {
--mat-toolbar-container-background-color: var(--mat-sys-surface-container);
}
注意最后一条的写法:component token 的值仍引用 system token。覆盖的是”引用”,不是”值”——这样明暗切换时它依然能跟着变。写死具体色值才是主题漂移的开始。
作用域主题:子树换色
system tokens 是普通 CSS 自定义属性,天然可继承、可重定义。给某个功能区域换一套强调色,只需要在该子树的根上重定义相关 token:
.promo-section {
// 该子树内所有消费 --mat-sys-primary 的 Material 组件自动变色
--mat-sys-primary: #b3261e;
--mat-sys-on-primary: #ffffff;
}
<section class="promo-section">
<button matButton>这个按钮跟着促销区变红</button>
</section>
<button matButton>这个按钮还是全局主色</button>
营销位、危险操作区(红)、成功态(绿)这类”局部换肤”需求,用作用域主题比写一套新主题便宜得多。但克制使用——页面里出现五种主色时,用户对”主色”的直觉就消失了。
完整的编译管线
把本篇所有机制串起来:
graph LR
P[palette 选择] --> T[mat.theme 生成全套 system tokens]
L[light overrides map] --> O
D[dark overrides map] --> O[mat.theme-overrides 按主题生效]
T --> CSS[CSS 自定义属性]
O --> CSS
CSS --> CT[component tokens 默认引用 system]
CT --> R[组件渲染]
SCOPE[子树重定义 - 作用域主题] --> R
定制检查清单
| 检查项 | 标准 |
|---|---|
| palette 优先 | 没有为”换个整体气质”而手写 20 个 token 覆盖 |
| 成组覆盖 | X 与 on-X 同改,无深底深字 |
| 值不写死 | component token 的值引用 system token,而非十六进制字面量 |
| 业务样式同源 | 自研组件同样消费 --mat-sys-*,不另起炉灶 |
| 双主题对称 | light 与 dark 的 overrides 覆盖相同的 token 集合 |
| 覆盖集中管理 | 所有 theme-overrides 收敛在 styles/ 目录,不散落在组件 scss 里 |
最后一条值得强调:token 覆盖是全局契约,散落在业务组件里等于埋雷。组件 scss 里只允许出现作用域主题(有明确边界)与组件级微调(有注释说明原因)。
下一篇进入具体组件:mat-form-field 这个”表单容器之王”的结构、appearance 与错误展示,以及它如何与 Signal Forms 的 [formField] 配合。