设计 Token 不该越堆越乱:三层分法和检查方式
让 AI 生成 UI 时,最容易丢掉的往往不是布局,而是设计系统的命名和层级。它会直接写一个接近的色值或间距,短时间看不出差别,主题切换和组件复用就开始变难。
Token 不必堆成一套庞大的术语体系。重点是分清基础值、语义和组件用途,再让构建与 lint 帮忙守住引用关系。本文以三层 Token、Style Dictionary 和 TypeScript 校验为例说明。
基础值、语义和组件用途分开
一个实用的 Token 体系通常分三层:基础值(Global)、语义(Semantic)和组件用途(Component)。叫法可以随团队约定,关键是引用关系清楚,主题切换时不必在组件里逐个找色值。
flowchart TD subgraph Figma Tokens / JSON 集中式配置源 A[JSON / YAML Token 原始设计文件] --> B{第一层: Option Tokens 基础选项层} B --> C[定义物理色板与纯数值: sys.color.palette.blue-500 = #3B82F6] C --> D{第二层: Alias Tokens 业务语义抽象层} D --> E[映射业务意图: sys.color.interactive.primary = {sys.color.palette.blue-500}] E --> F{第三层: Component Tokens 组件专有绑定层} F --> G[绑定组件属性: comp.button.primary.bg = {sys.color.interactive.primary}] end subgraph Style-Dictionary 自动化编译与多端分发管线 G --> H[Style-Dictionary 多端编译引擎] H --> I[Web 端: CSS Variables / SCSS / Tailwind] H --> J[iOS 端: Swift / Color Assets] H --> K[Android 端: XML / Jetpack Compose] H --> L[Flutter 端: Dart Color / ThemeData] end C -. ❌ 严禁越过语义层直接引用基础色板 .-> G三层各自负责的内容可以这样约定:
- 基础层:存色板、字号、间距等原始值,例如
blue-500、space-16。 - 语义层:描述用途,例如
interactive-primary、background-danger、surface-card,为主题映射留出位置。 - 组件层:表达组件属性,例如
button-primary-bg。只有组件确实需要独立状态或映射时才新增。
诊断命令与自动化 Lint 约束
构建和 lint 可以提醒代码是否绕过 Token。颜色与尺寸的硬编码不一定全是错误,例如边框或第三方组件的兼容处理;例外应写清原因,而不是靠一刀切规则压过去。
在仿真基准测试模型中,自动化诊断与拦截指令链如下:
# 1. 运行 Style-Dictionary 构建脚本,校验源 JSON 文件的结构合法性 npx style-dictionary build --config ./style-dictionary.config.js # 2. 执行 Stylelint 校验,阻断 CSS/SCSS 中的硬编码颜色与非标单位 npx stylelint "src/**/*.scss" --config .stylelintrc.tokens.js # 3. 运行自定义 Node.js 静态扫描脚本,分析未被 Alias 语义层收录的孤立 Token node scripts/audit-tokens-orphan.js --src="./src" --tokens="./build/web/tokens.json" --out=token-audit-report.json # 4. 从报告中提取违规硬编码样式的行号与关联选择器 jq '.violations[] | {file: .file, line: .line, raw_value: .rawValue}' token-audit-report.json这组命令能在合并前列出疑似绕过语义层的样式,评审再根据上下文决定保留、改 Token 或登记例外。
Style-Dictionary 编译转换与 TypeScript 强类型收束源码
下面的 Style-Dictionary 构建配置与 TypeScript 类型收束模块展示了如何将结构化 JSON Token 自动化编译为符合 Web 标准的 CSS 自定义变量,并提供编译期类型提示。
Style-Dictionary 编译配置文件 (style-dictionary.config.js)
const StyleDictionary = require('style-dictionary'); // 注册自定义命名转换器:生成 kebab-case 规范的 CSS 自定义变量名 StyleDictionary.registerTransform({ name: 'name/cti/kebab-semantic', type: 'name', transformer: (token) => { return `ds-${token.path.join('-')}`; } }); module.exports = { source: ['tokens/**/*.json'], platforms: { css: { transformGroup: 'css', transforms: ['attribute/cti', 'name/cti/kebab-semantic', 'color/hex'], buildPath: 'build/web/', files: [ { destination: 'tokens.css', format: 'css/variables', options: { showFileHeader: false, }, }, ], }, typescript: { transforms: ['attribute/cti', 'name/cti/kebab-semantic'], buildPath: 'build/web/', files: [ { destination: 'tokens.d.ts', format: 'typescript/es6-declarations', }, ], }, }, };强类型 Token 契约校验与解析模块 (token-contract-validator.ts)
import tokens from '../build/web/tokens.json'; export type SemanticColorMode = 'light' | 'dark'; export interface ButtonTokenContract { backgroundNormal: string; backgroundHover: string; textPrimary: string; borderRadius: string; } /** * 解析并生成契合设计 Token 的强类型样式属性映射 * @param isDark 是否开启暗黑模式 */ export function resolveButtonTokens(isDark: boolean): ButtonTokenContract { const modeKey: SemanticColorMode = isDark ? 'dark' : 'light'; // 校验源 JSON 结构中是否存在关键语义 Token,防止运行期样式破损 const interactiveTheme = tokens.sys?.color?.interactive?.[modeKey]; const radiusTheme = tokens.sys?.dimension?.radius; if (!interactiveTheme || !interactiveTheme.primary) { throw new Error(`[Token Schema Error] 关键语义 Token 缺失: sys.color.interactive.${modeKey}.primary`); } return { backgroundNormal: `var(--ds-sys-color-interactive-${modeKey}-primary)`, backgroundHover: `var(--ds-sys-color-interactive-${modeKey}-primary-hover)`, textPrimary: `var(--ds-sys-color-text-${modeKey}-on-primary)`, borderRadius: `var(--ds-sys-dimension-radius-${radiusTheme.medium || '8px'})`, }; }边界条件推演与典型反模式防范
维护 Token 时,下面三种情况最容易让体系失去边界:
1. 反模式一:按视觉属性命名(Visual-based Naming)
将 Token 命名为color-red-500并直接绑定到警告按钮、系统通知或错误输入框上。一旦后续品牌主题升级需要将警告主题色调整为深橙色或黄色,代码库中将产生color-red-500: #FF5722这种语义与实际值倒置的架构失真。
可行做法:组件引用时优先使用描述用途的语义 Token,例如color-feedback-danger;基础色板仍可保留在底层,供语义层映射。
2. 反模式二:Token 数量无节制爆炸(Token Explosion)
为了满足各个具体业务场景的微小视觉差异,允许每一个子组件都申请独立的 Component Token,会让 Token 库越来越难找、难改,构建产物也可能随之膨胀。
可行做法:创建组件 Token 前先确认现有语义 Token 是否已能表达需求。确有独立状态、主题映射或跨端差异时,再增加组件级 Token;数量上限应由实际维护成本决定。
3. 反模式三:缺乏平滑的版本废弃机制(Deprecation Strategy)
更新 Token 库时直接删除旧名称,会让仍在使用它的下游项目在升级后报错或样式丢失。
可行做法:加上@deprecated标注和编译期提醒,给下游一段明确的迁移窗口。窗口长短应按发布节奏和依赖范围决定:
{ "sys": { "color": { "old-btn-bg": { "$value": "{sys.color.interactive.primary}", "$extensions": { "deprecated": true, "replacement": "sys.color.interactive.primary-bg" } } } } }Token 变更怎么进入日常评审
日常规则可以简单些:业务组件引用语义 Token,而不是直接拿基础色板;颜色和尺寸的例外要有明确理由;新增全局 Token 先讨论用途,再写入构建产物。这样即使先用 AI 起草组件,也不会把设计系统悄悄拆散。