Material 3组件夹具封装教程 - 第1节
1.1 设计令牌(Design Tokens)——组件样式的原子单元
Material 3 的设计系统核心是 令牌(Tokens)。令牌是样式的最基本抽象层,它将颜色、字体、间距、圆角等设计决策定义为命名变量,使得所有组件都能通过引用这些变量获得统一的外观。令牌分为三个层级:
- 全局令牌(Global Tokens):与品牌无关的原始值,如
md.ref.palette.primary0(纯黑)、md.ref.typeface.plain(正文字体族)。 - 别名令牌(Alias Tokens):在全局令牌基础上映射到语义角色,如
md.sys.color.primary引用某个调色板色值,md.sys.typescale.title-large定义标题字号。 - 组件令牌(Component Tokens):直接作用于具体组件的属性,如
md.comp.button.container-color、md.comp.card.shape。
在组件夹具封装中,我们通常将组件令牌映射为 CSS 自定义属性或 TypeScript 常量,实现运行时主题切换。
💡 关键实现:在 React 组件中,通过
示例:从主题上下文中获取按钮容器颜色并应用到根节点。
useTheme Hook 读取令牌值,并注入到组件的 style 或 className 中。示例:从主题上下文中获取按钮容器颜色并应用到根节点。
令牌与CSS自定义属性映射
/* tokens.css —— 全局令牌定义 */
:root {
--md-ref-palette-primary0: #000000;
--md-ref-palette-primary10: #21005d;
--md-ref-palette-primary100: #ffffff;
/* … 更多全局色板 */
}
/* 别名令牌 —— 语义颜色 */
:root {
--md-sys-color-primary: var(--md-ref-palette-primary40);
--md-sys-color-on-primary: var(--md-ref-palette-primary100);
--md-sys-color-surface: var(--md-ref-palette-neutral99);
}
/* 组件令牌 —— 按钮容器颜色 */
:root {
--md-comp-button-container-color: var(--md-sys-color-primary);
--md-comp-button-disabled-container-color: rgba(0,0,0,0.12);
}
TypeScript 类型定义(夹具封装常用)
// tokens.types.ts
export interface MaterialTokens {
color: {
primary: string;
onPrimary: string;
surface: string;
error: string;
};
shape: {
small: string; // 圆角值,如"4px"
medium: string; // "8px"
large: string; // "16px"
};
typography: {
headlineLarge: string;
titleMedium: string;
bodyLarge: string;
};
}
1.2 组件层次结构——从原子到页面
Material 3 组件遵循 原子-分子-有机体 的层次模型。在夹具封装中,每一层都应当保持职责清晰。
| 层次 | 定义 | 封装示例 |
|---|---|---|
| 原子 | 最小组件单元,不可再分割(如按钮、图标、文本输入)。 | MdButton、MdIcon |
| 分子 | 由多个原子组合而成的通用控件(如搜索栏、卡片)。 | MdCard(内含按钮、图标、文本) |
| 有机体 | 业务相关的复合组件,通常包含状态管理(如用户资料卡片)。 | UserProfileCard(含数据加载、错误边界) |
封装原则:原子组件只接收令牌和基本回调;分子组件负责布局和子组件传参;有机体组件负责业务逻辑与数据流。
1.3 组件状态——交互响应模型
Material 3 为每个可交互组件定义了 状态层叠:enabled → hovered → focused → pressed → disabled。夹具封装需要为每个状态提供对应的视觉令牌和覆盖层。
⚠️ 常见错误:只处理 enabled 和 disabled 状态,忽略 hover/focus/pressed 的动画过渡,导致组件交互生硬。
解决方法:在组件样式表中为每个状态定义独立的 CSS 变量,并使用
解决方法:在组件样式表中为每个状态定义独立的 CSS 变量,并使用
transition 属性。
状态枚举与映射(React 夹具)
// component-states.ts
export enum ComponentState {
Enabled = 'enabled',
Hovered = 'hovered',
Focused = 'focused',
Pressed = 'pressed',
Disabled = 'disabled',
}
export function getStateToken(
baseToken: string,
state: ComponentState
): string {
const suffixMap: Record<ComponentState, string> = {
[ComponentState.Enabled]: '',
[ComponentState.Hovered]: '-hover',
[ComponentState.Focused]: '-focus',
[ComponentState.Pressed]: '-pressed',
[ComponentState.Disabled]: '-disabled',
};
return `${baseToken}${suffixMap[state]}`;
}
1.4 主题系统——暗色/亮色及自定义配色
Material 3 主题由三个核心维度构成:颜色方案(Color Scheme)、字体层级(Typography Scale)、形状体系(Shape Scale)。主题变量通常定义在 :root 或 [data-theme] 选择器下。
主题切换夹具实现步骤
- 定义全局 CSS 自定义属性(亮色/暗色两套)。
- 创建
ThemeProvider组件,管理当前主题类型(light/dark)。 - 在 Provider 中将主题令牌注入到
data-theme属性或:root选择器。 - 组件夹具通过
useContext获取当前主题对象,并应用到组件令牌变量。
主题 Provider 核心代码(React)
// ThemeProvider.tsx
import React, { createContext, useContext, useState } from 'react';
interface ThemeContextValue {
scheme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
scheme: 'light',
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [scheme, setScheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
const next = scheme === 'light' ? 'dark' : 'light';
setScheme(next);
document.documentElement.setAttribute('data-theme', next);
};
return (
<ThemeContext.Provider value={{ scheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
亮色/暗色主题 CSS 示例
/* 亮色主题(默认) */
:root {
--md-sys-color-primary: #6750a4;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-surface: #fffbfe;
--md-comp-button-container-color: var(--md-sys-color-primary);
--md-comp-button-shape: 20px;
}
/* 暗色主题 */
[data-theme="dark"] {
--md-sys-color-primary: #d0bcff;
--md-sys-color-on-primary: #381e72;
--md-sys-color-surface: #1c1b1f;
--md-comp-button-container-color: var(--md-sys-color-primary);
--md-comp-button-shape: 20px;
}
💡 调试技巧:在浏览器控制台使用
document.documentElement.setAttribute('data-theme', 'dark') 立即切换主题,验证所有组件是否正确响应令牌变化。
1.5 组件夹具的封装流程总览
结合本章知识点,一个完整的 Material 3 组件夹具封装流程如下:
| 步骤 | 操作 | 输出 |
|---|---|---|
| 1 | 定义全局令牌(颜色、字体、形状) | tokens.css 或 JSON 文件 |
| 2 | 创建主题 Provider 与上下文 | ThemeProvider.tsx + useTheme |
| 3 | 实现基础原子组件(只依赖令牌) | MdButton.tsx、MdIcon.tsx |
| 4 | 添加状态覆盖层(hover/focus/pressed/disabled) | CSS 伪类 + 状态令牌映射 |
| 5 | 组合分子组件(集成原子组件并处理布局) | MdCard.tsx、MdNavigationBar.tsx |
| 6 | 单元测试与视觉回归 | Jest + Storybook 快照 |
1.6 常见错误与稳定性优化
- 令牌名称不一致:确保组件使用的令牌名与主题定义完全匹配,建议使用 TypeScript 枚举或常量字符串(如
TOKENS.BUTTON.CONTAINER_COLOR)避免拼写错误。 - 状态层叠顺序错误:CSS 中必须按
enabled → hover → focus → press → disabled书写,否则后定义的状态可能被先定义的覆盖。 - 主题切换性能:避免在每次渲染时重新计算 CSS 变量,使用
data-theme属性批量切换,并利用 CSStransition实现平滑过渡。 - 形状单位不一致:所有圆角值统一使用 CSS 变量,避免直接写死 px 值,确保主题可定制。
稳定性检查清单(夹具发布前)
# 稳定性检查命令示例(基于 Playwright 视觉对比)
npx playwright test --update-snapshots # 更新基准图
npx playwright test # 对比当前快照
# 检查主题切换后所有组件无样式泄漏
npx lighthouse --chrome-flags="--headless" http://localhost:6006