从本篇开始进入 Angular Material 篇章。Material 22 基于 Material Design 3(M3)体系——v17 引入 M3 主题预览,v18 转正,此后所有组件的视觉输出都由一套 design token 驱动。本文解决”跑起来”与”看懂主题”两件事:安装、组件引入、M3 的 token 分层、预建主题、明暗模式切换、typography 与 density。下一篇讲深度定制,第 38 篇起逐个讲核心组件。
安装
ng add @angular/material
ng add 会完成四件事:安装 @angular/material 与 @angular/cdk(22 版本两者同版本号发布)、把主题引入 styles.scss、注册字体(Roboto 与 Material Icons/Symbols)、写入少量全局样式。全程交互式,选默认值即可。
v22 值得注意的减法:
- 不再需要
provideAnimations()或provideAnimationsAsync()——Material 组件已不依赖@angular/animations(个别保留动画的组件会自行处理) - 不再需要 HammerJS——触控手势早已内置
- 组件全部 standalone,
imports数组里写组件类而不是 Module(下文示例)
第一个页面
import { Component } from '@angular/core';
import { MatToolbar } from '@angular/material/toolbar';
import { MatButton } from '@angular/material/button';
@Component({
selector: 'app-home',
imports: [MatToolbar, MatButton],
template: `
<mat-toolbar>
<span>TaskBoard</span>
</mat-toolbar>
<main class="content">
<button matButton (click)="greet()">问好</button>
<button matButton="outlined">描边按钮</button>
<button matButton="text">文字按钮</button>
</main>
`,
styles: `
.content { padding: 16px; display: flex; gap: 8px; }
`,
})
export class HomeComponent {
greet(): void {
console.log('hello material 22');
}
}
两个新写法:组件按类引入(MatButton 而非 MatButtonModule),按钮变体用属性选择(matButton="outlined",v20 起取代旧的 mat-stroked-button)。颜色无需配置——按钮用的 primary 色来自主题 token。
M3 主题体系:三层 token
Material 3 的核心思想是组件不写死颜色,只消费 token。token 分三层:
graph TD
P[palette 调色板 - mat.$azure-palette 等] --> S[system tokens - --mat-sys-*]
S --> C[component tokens - --mat-<comp>-*]
C --> W[组件渲染]
O[mat.theme-overrides / CSS 变量覆盖] -.-> S
- palette:一套 M3 调色板(tonal palette),是”原料”
- system tokens:
--mat-sys-primary、--mat-sys-surface、--mat-sys-body-large-font等全局语义变量,由mat.theme从 palette 生成,是主题定制的主要抓手 - component tokens:每个组件自己的变量(如 button 的容器高度),默认值引用 system tokens,一般只在做组件级微调时碰(第 37 篇)
这意味着换主题 = 换一组 CSS 变量,组件代码零改动;自定义组件也可以直接消费 var(--mat-sys-primary) 保持与 Material 视觉一致。
预建主题:一行起步
ng add 默认写入的就是预建主题(在 styles.scss 中):
@use '@angular/material/prebuilt-themes/azure-blue.css';
官方提供四组:azure-blue、magenta-violet、rose-red、cyan-orange。预建主题同时包含浅色与深色两套 token,跟随 color-scheme(下文)切换。它们的价值是零学习成本跑通页面;一旦要动品牌色,就升级到自定义主题。
自定义主题:mat.theme
把预建主题那行换成 mat.theme,指定颜色、字体、密度三根轴:
@use '@angular/material' as mat;
html {
color-scheme: light dark;
@include mat.theme((
color: (
primary: mat.$violet-palette,
tertiary: mat.$orange-palette,
),
typography: Roboto,
density: 0,
));
}
primary主色板、tertiary强调色板,从官方预置的十余组 palette(mat.$azure-palette、mat.$violet-palette、mat.$rose-palette、mat.$green-palette等)中选取typography接受字体族字符串或配置 map(下文)density取0到-4,每降一级组件高度约减 4px(下文)
这个 mixin 做的事就是生成全套 --mat-sys-* 变量。选 palette 只是定制的第一级——覆盖具体 token 用 mat.theme-overrides,这是下一篇的主角,这里先见个面:
html {
@include mat.theme((
color: (primary: mat.$violet-palette, tertiary: mat.$orange-palette),
));
// 精修:把主色与主色上的文字微调成品牌值
@include mat.theme-overrides((
primary: #5b3cc4,
on-primary: #ffffff,
));
}
明暗模式切换
M3 token 天生成对:每个颜色 token 都有 light/dark 两个值,浏览器按 color-scheme 选择当前生效的一套。于是明暗切换有三种实现层次:
| 策略 | 实现 | 用户体验 |
|---|---|---|
| 跟随系统 | html { color-scheme: light dark; } | 跟 OS 设置自动切换,无 UI |
| 手动强制 | [data-theme='dark'] { color-scheme: dark; } | 应用内开关,忽略系统 |
| 三态开关 | 系统浅色 / 系统深色 / 手动 | 最常见的产品要求 |
推荐第三种。CSS 侧:
html {
color-scheme: light dark;
@include mat.theme((
color: (primary: mat.$violet-palette, tertiary: mat.$orange-palette),
));
}
// 手动选择时覆盖 color-scheme
html[data-theme='light'] { color-scheme: light; }
html[data-theme='dark'] { color-scheme: dark; }
TS 侧一个信号驱动的服务(复用第 21 篇的 effect 持久化模式):
import { Service, effect, signal } from '@angular/core';
export type ThemePreference = 'system' | 'light' | 'dark';
@Service()
export class ThemeService {
readonly preference = signal<ThemePreference>(
(localStorage.getItem('theme') as ThemePreference) ?? 'system',
);
constructor() {
effect(() => {
const pref = this.preference();
localStorage.setItem('theme', pref);
const root = document.documentElement;
if (pref === 'system') {
delete root.dataset['theme'];
} else {
root.dataset['theme'] = pref;
}
});
}
toggle(): void {
const order: ThemePreference[] = ['system', 'light', 'dark'];
const next = order[(order.indexOf(this.preference()) + 1) % order.length];
this.preference.set(next);
}
}
开关按钮:
<button matButton="icon" (click)="theme.toggle()" [attr.aria-label]="'主题:' + theme.preference()">
{{ theme.preference() }}
</button>
注意:自己写的业务样式也要跟上 token——背景用 var(--mat-sys-surface)、文字用 var(--mat-sys-on-surface),而不是写死 #fff/#333,否则明暗切换时 Material 组件变了、你的页面没变。
Typography
mat.theme 的 typography 轴生成 --mat-sys-{display|headline|title|body|label}-{large|medium|small}-* 全套字体 token。两种写法:
// 简写:统一字体族
@include mat.theme((
color: (primary: mat.$violet-palette),
typography: Roboto,
));
// 展开写法:区分 UI 字与品牌字、加重粗细
@include mat.theme((
color: (primary: mat.$violet-palette),
typography: (
plain-family: Roboto, // 正文/UI
brand-family: 'Open Sans', // 标题/display
bold-weight: 600,
),
));
字体文件在 index.html 引入:
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@400;600&display=swap"
rel="stylesheet"
/>
业务文本消费 token 的方式与颜色相同:font-family: var(--mat-sys-body-large-font)、font-size: var(--mat-sys-body-large-size)。M3 用”角色”(display/headline/title/body/label)而不是像素值组织字号,用 token 就是与设计体系对齐。
Density
密度轴控制组件的”紧凑度”:
@include mat.theme((
color: (primary: mat.$violet-palette),
density: -1, // 0(默认舒适)到 -4(最紧凑),每级约 -4px 高度
));
| 值 | 场景 |
|---|---|
| 0 | 默认,移动端与触控友好 |
| -1 | 桌面中后台,多数团队的选择 |
| -2 | 数据密集型表格、监控大屏 |
| -3 / -4 | 极端紧凑,观感牺牲明显,慎用 |
density 只影响组件尺寸类 token,不改变颜色与字号,可以与明暗主题独立组合。
版本沿革速查
| 版本 | 主题能力 |
|---|---|
| v16 及以前 | M2 主题:mat.define-light-theme 三段式 |
| v17 | M3 主题预览,新 Sass API 起步 |
| v18 | M3 稳定:mat.theme + --mat-sys-* token 体系成为默认 |
| v22 | 本篇基线:token 体系成熟,组件全面 token 化 |
从 M2 迁移到 M3 的项目注意:旧 API(define-light-theme、define-palette)在 M3 Sass 里以兼容形态保留了一段时间,但新代码一律写 mat.theme,不要混用两套体系。
小结
Material 22 的主题心智模型一句话:mat.theme 生成 system tokens,组件消费 tokens,明暗切换是 color-scheme,定制是覆盖 tokens。跑通本篇后,你已经能控制”像不像 Material”的整体观感;下一篇深入 token 的逐项定制——品牌色怎么精修、组件级 token 怎么覆盖、双主题的 Sass 文件怎么组织。