设计 Token 系统建设:从颜色变量到设计决策的工程化体系
设计 Token 系统建设:从颜色变量到设计决策的工程化体系
一、Token 不是变量:从样式复用到设计决策的抽象
设计 Token 常被误解为"CSS 变量的另一种写法"。但 Token 的本质是设计决策的抽象——--color-primary不是"蓝色",而是"品牌主色",蓝色只是当前的决策值。当品牌升级时,只需修改 Token 的值,所有引用该 Token 的组件自动更新。这种抽象层级比 CSS 变量更高——CSS 变量是技术实现,Token 是设计语义。
Token 系统的层级结构:全局 Token(Global Token)定义原始值(如--gray-900: #1a1a2e),别名 Token(Alias Token)定义语义(如--color-text-primary: var(--gray-900)),组件 Token(Component Token)定义组件级样式(如--button-bg: var(--color-primary))。三级结构确保了修改的影响范围可控。
二、Token 系统架构:三级抽象与主题切换
Token 系统的核心是三级抽象:Global → Alias → Component。Global Token 是最底层的原始值,不包含语义;Alias Token 赋予语义,是设计系统的核心;Component Token 将 Alias Token 绑定到具体组件,实现组件级定制。
flowchart TB subgraph Global Token A1[--gray-900: #1a1a2e] A2[--blue-500: #3b82f6] A3[--spacing-4: 16px] A4[--radius-md: 8px] end subgraph Alias Token B1[--color-text-primary: var(--gray-900)] B2[--color-primary: var(--blue-500)] B3[--spacing-md: var(--spacing-4)] B4[--radius-default: var(--radius-md)] end subgraph Component Token C1[--button-bg: var(--color-primary)] C2[--button-padding: var(--spacing-md)] C3[--button-radius: var(--radius-default)] C4[--button-text: var(--color-text-on-primary)] end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B4 B2 --> C1 B3 --> C2 B4 --> C3 B1 --> C4 subgraph 主题切换 D[Light Theme] E[Dark Theme] end D --> B1 E --> B1主题切换的实现原理:不同主题覆盖 Alias Token 的值。Light 主题下--color-bg指向--white,Dark 主题下指向--gray-900。Global Token 不变,Alias Token 随主题切换,Component Token 自动跟随。
三、生产级代码实现:Token 定义、主题系统与组件绑定
3.1 Token 定义文件
/* ======================================== Global Token: 原始值,不包含语义 为什么分离 Global 和 Alias:Global Token 是设计系统的"调色板",修改它影响全局; Alias Token 是"语义层",修改它只影响 特定语义场景。分离后修改更安全 ======================================== */ :root { /* 颜色 - 灰度 */ --gray-50: #f8f9fa; --gray-100: #f1f3f5; --gray-200: #e9ecef; --gray-300: #dee2e6; --gray-400: #ced4da; --gray-500: #adb5bd; --gray-600: #868e96; --gray-700: #495057; --gray-800: #343a40; --gray-900: #212529; /* 颜色 - 品牌色 */ --blue-50: #e7f5ff; --blue-100: #d0ebff; --blue-200: #a5d8ff; --blue-300: #74c0fc; --blue-400: #4dabf7; --blue-500: #339af0; --blue-600: #228be6; --blue-700: #1c7ed6; --blue-800: #1971c2; --blue-900: #1864ab; /* 间距 */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; /* 圆角 */ --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px; --radius-full: 9999px; /* 阴影 */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); /* 字体 */ --font-sans: "Inter", system-ui, -apple-system, sans-serif; --font-mono: "JetBrains Mono", "Fira Code", monospace; /* 字号 */ --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; --text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem; /* 行高 */ --leading-tight: 1.25; --leading-normal: 1.5; --leading-relaxed: 1.75; }3.2 Alias Token 与主题
/* ======================================== Alias Token: 语义层,主题切换的锚点 ======================================== */ /* Light 主题(默认) */ :root, [data-theme="light"] { /* 语义色 */ --color-bg: var(--gray-50); --color-bg-elevated: var(--white); --color-bg-sunken: var(--gray-100); --color-text-primary: var(--gray-900); --color-text-secondary: var(--gray-600); --color-text-tertiary: var(--gray-400); --color-text-on-primary: var(--white); --color-primary: var(--blue-600); --color-primary-hover: var(--blue-700); --color-primary-active: var(--blue-800); --color-primary-subtle: var(--blue-50); --color-border: var(--gray-300); --color-border-focus: var(--blue-500); /* 语义间距 */ --spacing-inline-xs: var(--space-1); --spacing-inline-sm: var(--space-2); --spacing-inline-md: var(--space-4); --spacing-inline-lg: var(--space-6); --spacing-stack-xs: var(--space-1); --spacing-stack-sm: var(--space-2); --spacing-stack-md: var(--space-4); --spacing-stack-lg: var(--space-6); } /* Dark 主题 */ /* 为什么 Dark 主题只覆盖 Alias Token: Global Token 是原始值,跨主题不变; 只覆盖 Alias Token 确保修改范围可控, 且组件 Token 自动跟随 Alias Token 变化 */ [data-theme="dark"] { --color-bg: var(--gray-900); --color-bg-elevated: var(--gray-800); --color-bg-sunken: var(--gray-950); --color-text-primary: var(--gray-50); --color-text-secondary: var(--gray-400); --color-text-tertiary: var(--gray-600); --color-text-on-primary: var(--gray-900); --color-primary: var(--blue-400); --color-primary-hover: var(--blue-300); --color-primary-active: var(--blue-200); --color-primary-subtle: var(--blue-900); --color-border: var(--gray-700); --color-border-focus: var(--blue-400); }3.3 Component Token 与组件实现
/* ======================================== Component Token: 组件级样式绑定 ======================================== */ /* Button 组件 Token */ .button { /* 组件 Token 定义 */ --button-bg: var(--color-primary); --button-bg-hover: var(--color-primary-hover); --button-bg-active: var(--color-primary-active); --button-text: var(--color-text-on-primary); --button-padding-x: var(--spacing-inline-md); --button-padding-y: var(--spacing-stack-sm); --button-radius: var(--radius-md); --button-font: var(--font-sans); --button-font-size: var(--text-sm); --button-font-weight: 500; /* 使用 Component Token */ background: var(--button-bg); color: var(--button-text); padding: var(--button-padding-y) var(--button-padding-x); border-radius: var(--button-radius); font-family: var(--button-font); font-size: var(--button-font-size); font-weight: var(--button-font-weight); border: none; cursor: pointer; transition: background 0.15s ease; } .button:hover { background: var(--button-bg-hover); } .button:active { background: var(--button-bg-active); } /* 为什么用 Component Token 而非直接用 Alias Token: 直接用 Alias Token 时,修改 Alias Token 会 影响所有引用它的组件;Component Token 允许 单个组件覆盖样式而不影响其他组件 */ .button--secondary { --button-bg: transparent; --button-bg-hover: var(--color-primary-subtle); --button-text: var(--color-primary); --button-border: var(--color-primary); }3.4 Token 管理工具
// Token 管理器:校验、转换和同步 class DesignTokenManager { constructor(tokens) { this.tokens = tokens; } // 校验 Token 的完整性 validate() { const errors = []; // 检查所有 Alias Token 是否引用了存在的 Global Token // 为什么需要校验:Token 引用链断裂会导致 // 样式失效,且难以排查(CSS 不会报错, // 只是回退到默认值) for (const [name, value] of Object.entries(this.tokens.alias)) { if (typeof value === "string" && value.startsWith("var(")) { const ref = value.match(/var\(([^,)]+)\)/)?.[1]; if (ref && !this.tokens.global[ref] && !this.tokens.alias[ref]) { errors.push(`Alias Token "${name}" 引用了不存在的 Token "${ref}"`); } } } return errors; } // 生成 CSS 变量声明 toCSS(theme = "light") { const lines = []; lines.push(":root,"); // Global Token for (const [name, value] of Object.entries(this.tokens.global)) { lines.push(` ${name}: ${value};`); } // Alias Token(按主题) lines.push(`[data-theme="${theme}"] {`); for (const [name, value] of Object.entries(this.tokens.alias[theme])) { lines.push(` ${name}: ${value};`); } lines.push("}"); return lines.join("\n"); } // 生成 Tailwind 配置 toTailwindConfig() { // 为什么支持 Tailwind 输出:Tailwind 的 // 配置与 Token 系统对齐,避免两套体系 return { colors: this._extractColors(), spacing: this._extractSpacing(), borderRadius: this._extractRadius(), fontSize: this._extractFontSizes(), }; } }四、Token 系统的架构权衡:粒度、命名与同步成本
Token 粒度的权衡:Token 越细,定制能力越强,但维护成本越高。一个 Button 组件可以有 20 个 Component Token(背景、文字、边框、阴影、圆角、内边距……),也可以只有 3 个(variant、size、state)。建议核心组件(Button、Input、Card)用细粒度 Token,辅助组件用粗粒度 Token。
命名规范的一致性:Token 命名必须遵循统一的规则,否则团队无法快速理解 Token 的含义。推荐格式:{类别}-{属性}-{变体}-{状态},如--color-primary-hover。避免使用具体颜色名(如--blue-500)作为 Alias Token。
设计工具与代码的同步:Figma 中的样式变量和代码中的 Token 需要保持同步。手动同步容易遗漏,建议使用 Style Dictionary 或 Tokens Studio 等工具自动转换。同步是 Token 系统最大的运维成本。
多平台 Token 的统一:Web(CSS 变量)、iOS(Swift 颜色/间距常量)、Android(XML 资源)的 Token 格式不同。Style Dictionary 可以从统一的 JSON 源文件生成各平台的 Token 文件,是跨平台 Token 管理的标准方案。
五、总结
设计 Token 系统的核心是三级抽象:Global Token 定义原始值,Alias Token 赋予语义,Component Token 绑定组件。主题切换通过覆盖 Alias Token 实现,Component Token 自动跟随。落地时建议先建立颜色和间距的 Token 体系,再逐步扩展到排版、阴影和动效。命名规范和设计工具同步是长期运维的关键,建议在项目初期就建立自动化流程。
