设计系统建设
设计系统不是组件库
这是最常见的认知偏差。很多团队说"我们要建设计系统",实际做的是"封装一批 UI 组件发到 npm"。组件库是设计系统的技术产出之一,但设计系统本身是一套活的体系——包含设计原则、视觉语言、交互模式、工程规范、文档流程和治理机制。
一个成熟的设计系统至少包含这些层次:
Design Principles(设计原则)
"我们的产品应该是什么感觉?"
↓
Visual Language(视觉语言)
色彩体系、字体排版、间距节奏、阴影层级、动效曲线
↓
Design Tokens(设计令牌)
将视觉语言编码为平台无关的键值对
↓
Component Library(组件库)
基于 tokens 实现的可复用 UI 组件
↓
Pattern Library(模式库)
组件的组合方式——表单模式、导航模式、数据展示模式
↓
Documentation & Tooling(文档与工具链)
Storybook、使用指南、贡献流程、版本日志
↓
Governance(治理机制)
谁决定加什么组件?谁审核 API 变更?如何处理 breaking change?
判断标准:如果你的"设计系统"只有组件代码没有 tokens、没有文档、没有贡献流程,那它只是一个组件库。组件库解决的是"不重复造轮子",设计系统解决的是"产品体验一致性 + 团队协作效率"。
Design Tokens 深度解析
什么是 Design Token
Design Token 是设计决策的最小原子单元,以平台无关的格式存储。这个概念由 Salesforce 设计系统团队(Jina Anne & Jon Levine)在 2014 年提出。
核心思想:设计决策不应该硬编码在组件里,而应该抽象为可复用、可转换的数据。
设计师说:"主色调是蓝色"
→ Token: color.primary = #3b82f6
→ CSS: --color-primary: #3b82f6
→ iOS: UIColor(hex: "#3b82f6")
→ Android: @color/primary = #3b82f6
→ Figma: Variable "color/primary"
Token 分层架构
成熟的 token 体系通常分三层:
┌─────────────────────────────────────────────┐
│ Semantic Tokens(语义层) │
│ color.text.primary → {reference: blue.700} │
│ color.bg.danger → {reference: red.100} │
│ spacing.page.gutter → {reference: space.6} │
├─────────────────────────────────────────────┤
│ Alias / Scale Tokens(别名/刻度层) │
│ blue.700 → #1d4ed8 │
│ red.100 → #fee2e2 │
│ space.6 → 24px │
├─────────────────────────────────────────────┤
│ Primitive Tokens(原始值层) │
│ #1d4ed8, #fee2e2, 24px, 16px, 400ms │
└─────────────────────────────────────────────┘
为什么要分层? 因为多品牌/多主题场景下,你只需要替换 Semantic → Alias 的映射关系,而不需要改动任何组件代码。比如品牌 A 的 color.text.primary 指向 blue.700,品牌 B 指向 green.700,组件代码完全不变。
W3C DTCG 规范(2025.10 稳定版)
2025 年 10 月,W3C Design Tokens Community Group(DTCG)发布了第一个稳定版规范,这是设计系统领域的里程碑事件。规范定义了 token 的标准 JSON 格式:
{
"color": {
"primary": {
"$value": "#3b82f6",
"$type": "color",
"$description": "Brand primary color"
},
"background": {
"default": {
"$value": "#ffffff",
"$type": "color"
},
"subtle": {
"$value": "#f8fafc",
"$type": "color"
}
}
},
"spacing": {
"sm": {
"$value": "8px",
"$type": "dimension"
},
"md": {
"$value": "16px",
"$type": "dimension"
}
},
"duration": {
"fast": {
"$value": "150ms",
"$type": "duration"
}
}
}
规范的关键设计决策:
$前缀:所有元数据字段用$前缀($value、$type、$description),避免与 token 名称冲突- 类型系统:定义了 color、dimension、duration、fontFamily、fontWeight、cubicBezier、shadow 等标准类型
- 引用语法:
{color.primary}表示引用另一个 token 的值,支持 token 间的依赖关系 - 分组:通过 JSON 嵌套自然形成分组,不需要额外的分组语法
工程影响:Figma 已经原生支持 DTCG 格式的 Variables 导入导出,Style Dictionary 4.x 也已全面支持。这意味着设计师在 Figma 中定义的 Variables 可以直接导出为标准格式,再通过工具链编译为各平台代码。
Token 工具链
Figma Variables
↓ 导出 DTCG JSON
Style Dictionary 4.x
↓ 转换 + 编译
┌──────────┬──────────┬──────────┐
│ CSS Vars │ JS/TS │ iOS/ │
│ │ Constants│ Android │
└──────────┴──────────┴──────────┘
Style Dictionary 是 Amazon 开源的 token 编译工具,核心能力是将一份 token 定义转换为多个平台的输出格式:
// style-dictionary.config.js
export default {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [{
destination: 'variables.css',
format: 'css/variables',
}],
},
js: {
transformGroup: 'js',
buildPath: 'dist/js/',
files: [{
destination: 'tokens.js',
format: 'javascript/es6',
}],
},
},
};
组件库架构决策
核心架构选择
建设组件库时需要做一系列关键的架构决策,每个决策都有明确的 trade-off:
| 决策维度 | 选项 A | 选项 B | 判断依据 |
|---|---|---|---|
| 包结构 | Monorepo 多包(@ds/button) |
单包(@ds/components) |
团队规模 > 5 人或组件 > 30 个用 monorepo |
| 样式方案 | CSS Variables + Utility(Tailwind) | CSS-in-JS(Styled/Emotion) | 2025 趋势明显偏向 CSS Variables |
| 无障碍基础 | Radix Primitives / React Aria | 自研 | 除非有极特殊需求,否则不要自研 a11y |
| 状态管理 | 组件内部 Context | 外部 store 注入 | 组件库应自包含,不依赖外部 store |
| 版本策略 | 独立版本(independent) | 统一版本(fixed) | 独立版本灵活但管理复杂 |
| 发布流程 | Changesets + CI | 手动发布 | 任何正经项目都应该用自动化发布 |
Headless 优先:现代组件库的基础层
2024-2025 年,headless component 已经成为组件库建设的主流基础层。核心理念是逻辑与样式分离——组件库提供行为、状态管理和无障碍支持,样式完全由消费者控制。
主流 headless 方案对比:
| 库 | 维护方 | 特点 |
|---|---|---|
| Radix Primitives | WorkOS | React 专用,API 设计优雅,Compound Component 模式 |
| React Aria | Adobe | 最严格的 WAI-ARIA 合规,hooks-based API |
| Headless UI | Tailwind Labs | 为 Tailwind 优化,React + Vue |
| Ark UI | Chakra 团队 | 基于状态机(Zag.js),React + Vue + Solid |
为什么 headless 是正确的基础层?
- 样式自由:消费者可以用 Tailwind、CSS Modules、vanilla CSS,不被组件库的样式方案绑定
- 无障碍内建:键盘导航、ARIA 属性、焦点管理这些复杂逻辑由专业团队维护
- 行为一致:不同主题/品牌共享同一套交互行为
- 减少维护负担:你的团队只需要关注样式层,不需要处理 a11y 的边界情况
shadcn/ui 的成功就是建立在 Radix Primitives 之上——它本质上是 Radix + Tailwind 的一套预设样式。
Monorepo 组件库结构
packages/
├── tokens/ ← Design Tokens 包
│ ├── src/tokens.json
│ ├── dist/css/variables.css
│ └── dist/js/tokens.js
├── primitives/ ← Headless 基础组件(或直接用 Radix)
│ ├── dialog/
│ ├── popover/
│ └── select/
├── components/ ← 带样式的组件
│ ├── button/
│ │ ├── Button.tsx
│ │ ├── Button.styles.ts
│ │ ├── Button.test.tsx
│ │ └── Button.stories.tsx
│ ├── input/
│ └── modal/
├── icons/ ← 图标包
├── hooks/ ← 共享 hooks
└── eslint-config/ ← 共享 lint 配置
apps/
├── docs/ ← Storybook 文档站
└── playground/ ← 开发调试用
Storybook 驱动开发
不只是文档工具
Storybook 在设计系统中的角色远不止"展示组件"。它是组件开发的主要工作台:
- 隔离开发:在 Storybook 中开发组件,不需要启动整个应用
- 状态覆盖:通过 Stories 覆盖组件的所有状态(loading、error、empty、overflow)
- 视觉回归测试:配合 Chromatic 做自动化截图对比
- 交互测试:Storybook 8 内置 play function,可以在 story 中编写交互测试
- 文档生成:从 TypeScript 类型自动生成 Props 文档
Story 编写范式
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'danger'],
description: '按钮变体',
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
},
args: {
children: 'Button',
variant: 'primary',
size: 'md',
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// 基础变体
export const Primary: Story = {
args: { variant: 'primary' },
};
export const Secondary: Story = {
args: { variant: 'secondary' },
};
// 状态覆盖
export const Loading: Story = {
args: { loading: true },
};
export const Disabled: Story = {
args: { disabled: true },
};
// 交互测试(Storybook 8 play function)
export const ClickInteraction: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(button).toHaveFocus();
},
};
视觉回归测试
Chromatic(Storybook 团队的商业产品)或开源方案(Loki、Percy)可以对每个 story 做截图对比:
PR 提交 → CI 触发 Chromatic → 对比所有 stories 的截图
→ 无变化:自动通过
→ 有变化:生成 diff 图,需要人工审核 approve
这解决了 CSS 修改的"蝴蝶效应"问题——改了一个 token 值,Chromatic 会告诉你哪些组件的视觉发生了变化。
多品牌与主题系统
CSS Custom Properties 驱动的主题切换
现代主题系统的核心是 CSS Custom Properties(CSS 变量)。相比 CSS-in-JS 的运行时主题切换,CSS 变量是零运行时开销的:
/* tokens.css — 默认主题(Light) */
:root {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8fafc;
--color-text-primary: #0f172a;
--color-text-secondary: #64748b;
--color-border: #e2e8f0;
--color-accent: #3b82f6;
--radius-md: 8px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* Dark 主题 — 只覆盖变量值 */
[data-theme="dark"] {
--color-bg-primary: #0f172a;
--color-bg-secondary: #1e293b;
--color-text-primary: #f8fafc;
--color-text-secondary: #94a3b8;
--color-border: #334155;
--color-accent: #60a5fa;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* 品牌 B — 替换品牌色 */
[data-brand="brand-b"] {
--color-accent: #10b981;
--radius-md: 12px;
}
组件代码只引用语义变量,完全不关心当前是什么主题或品牌:
.button-primary {
background: var(--color-accent);
color: var(--color-bg-primary);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
多品牌架构模式
tokens/
├── global/ ← 所有品牌共享的 token(间距、字体大小刻度)
│ └── spacing.json
├── brand-a/ ← 品牌 A 的语义 token
│ ├── color.json
│ └── typography.json
├── brand-b/ ← 品牌 B 的语义 token
│ ├── color.json
│ └── typography.json
└── themes/ ← 每个品牌的 light/dark 变体
├── brand-a-light.json
├── brand-a-dark.json
├── brand-b-light.json
└── brand-b-dark.json
构建时,Style Dictionary 根据品牌 + 主题组合编译出对应的 CSS 文件。组件代码只有一份,通过 CSS 变量实现视觉差异。
shadcn/ui 范式:从"安装依赖"到"拥有代码"
传统组件库的困境
传统组件库(Ant Design、MUI、Chakra UI)的模式是:
npm install @chakra-ui/react
消费者通过 npm 包使用组件,通过 props 和 theme 配置来定制。这个模式的问题:
- 定制天花板:props 没暴露的能力就无法定制,除非 fork
- 样式覆盖地狱:
!important、深层选择器覆盖、specificity 战争 - 版本升级痛苦:major version 升级经常是大规模 breaking change
- bundle size:即使只用 5 个组件,也可能引入整个库的样式
shadcn/ui 的解法
shadcn/ui 不是一个 npm 包,而是一个代码生成器:
npx shadcn@latest add button
# → 将 Button 组件的源代码复制到你的项目中
# → 你拥有这份代码,可以任意修改
本质上,shadcn/ui = Radix Primitives(headless 行为层)+ Tailwind CSS(样式层)+ 一套精心设计的默认样式。
这个模式为什么成功?
- 完全可控:代码在你的仓库里,想改什么改什么
- 零抽象泄漏:没有 theme provider、没有 CSS-in-JS runtime、没有样式覆盖问题
- 渐进式采用:需要哪个组件就加哪个,不是 all-or-nothing
- 学习价值:可以直接阅读组件源码,理解最佳实践
trade-off:你需要自己维护这些组件代码。当 shadcn/ui 更新了某个组件的 bug fix,你需要手动同步。这对小团队是优势(灵活),对大团队可能是劣势(维护成本)。
对设计系统建设的启示
shadcn/ui 的成功说明了一个趋势:组件库的价值不在于"封装和分发",而在于"最佳实践的沉淀"。
对于企业内部设计系统,可以借鉴这个思路:
- 核心 primitives(headless 行为层)作为 npm 包分发,保证行为一致
- 样式层提供 CLI 工具生成到业务项目中,允许业务团队定制
- Token 作为独立包分发,保证视觉一致性
设计系统治理
团队模型
Nathan Curtis 提出了三种设计系统团队模型:
| 模型 | 描述 | 适用场景 |
|---|---|---|
| Centralized(集中式) | 专职团队拥有设计系统,其他团队是消费者 | 大公司,产品线多,需要强一致性 |
| Federated(联邦式) | 各产品团队派代表组成虚拟团队,共同维护 | 中型公司,需要平衡一致性和灵活性 |
| Distributed(分布式) | 没有专职团队,各团队自发贡献 | 小公司或开源项目 |
大多数成功的设计系统最终会演化为 Centralized + Federated 混合模式:有一个核心团队负责 tokens、primitives 和发布流程,各产品团队通过 RFC 流程贡献新组件。
组件生命周期管理
Proposal(提案)
→ 有人提出需要新组件,写 RFC 说明用途和 API 设计
↓
Review(评审)
→ 设计系统团队 + 设计师 + 主要消费团队评审 API
↓
Alpha(实验阶段)
→ 实现组件,发布 alpha 版本,在 1-2 个产品中试用
↓
Beta(稳定化)
→ 根据试用反馈调整 API,补充测试和文档
↓
Stable(正式发布)
→ 进入正式版本,API 受 semver 保护
↓
Deprecated(废弃)
→ 标记 @deprecated,提供迁移指南,至少保留 2 个 major version
版本策略与发布
Changesets 是目前最流行的 monorepo 版本管理工具:
# 开发者完成功能后,运行 changeset 命令
npx changeset
# → 交互式选择影响的包
# → 选择 semver 级别(patch/minor/major)
# → 写变更描述
# CI 自动收集所有 changesets,生成版本号和 CHANGELOG
npx changeset version
npx changeset publish
关键原则:
- 组件 API 变更必须遵循 semver:新增 prop 是 minor,删除/重命名 prop 是 major
- Token 变更要谨慎:删除一个 token 等于 breaking change,因为消费者可能直接引用
- 视觉变更也是变更:改了按钮的圆角默认值,虽然 API 没变,但视觉回归了,应该是 minor
Figma ↔ Code 工作流
设计师与开发者的协作断层
传统工作流的痛点:
设计师在 Figma 中定义颜色 → 开发者手动抄到 CSS 中
→ 设计师改了颜色 → 开发者不知道 → 产品视觉不一致
现代工作流:Figma Variables → DTCG JSON → Code
2024-2025 年,Figma 原生支持了 Variables 功能,并且可以导入/导出 DTCG 格式的 JSON。这使得以下工作流成为可能:
Figma Variables(设计师维护)
↓ Figma REST API / 插件导出
DTCG JSON(版本控制在 Git 中)
↓ Style Dictionary 编译
CSS Variables + JS Constants
↓ npm publish
组件库 + 业务项目消费
关键点:Token 的 source of truth 可以在 Figma 中(设计师主导),也可以在 JSON 文件中(开发者主导)。选择哪种取决于团队的协作模式。大多数团队选择 Figma 为 source of truth,因为设计师需要在 Figma 中直接使用这些 Variables 来设计界面。
常见误区与工程判断
误区一:过早建设设计系统
如果你的产品还在 PMF(Product-Market Fit)阶段,频繁 pivot,这时候建设计系统是浪费。设计系统的 ROI 来自规模化复用——当你有 3+ 个产品或 10+ 个开发者时,投入才开始回本。
误区二:追求完美的 Token 体系
很多团队花几个月设计"完美的" token 命名体系,结果发现实际使用时总有覆盖不到的场景。务实的做法是:从 Tailwind 的 scale 开始,遇到不够用的再扩展。Tailwind 的 spacing/color scale 已经经过了大量项目验证。
误区三:自研 headless 组件
键盘导航、焦点管理、ARIA 属性、屏幕阅读器兼容——这些东西的复杂度远超想象。一个 <Select> 组件要正确处理的 a11y 场景可能有 50+ 个。用 Radix 或 React Aria,把精力放在样式和业务逻辑上。
误区四:组件库 = 设计系统
前面已经说过,但值得再强调:没有 tokens、没有文档、没有治理流程的组件库,在跨团队协作时会迅速失控。每个团队都会 fork 出自己的版本,最终比没有组件库更混乱。
误区五:忽视组件 API 的向后兼容
组件的 props 就是它的 API 契约。一旦发布了 stable 版本,每次 props 变更都需要考虑向后兼容:
// ❌ Breaking change — 重命名 prop
// v1: <Button type="primary" />
// v2: <Button variant="primary" /> ← 所有消费者都要改
// ✅ 渐进式迁移
interface ButtonProps {
/** @deprecated 使用 variant 代替 */
type?: 'primary' | 'secondary';
variant?: 'primary' | 'secondary';
}
function Button({ type, variant, ...props }: ButtonProps) {
const resolvedVariant = variant ?? type ?? 'primary';
if (type) {
console.warn('Button: "type" prop is deprecated, use "variant" instead');
}
// ...
}
设计系统成熟度模型
| 阶段 | 特征 | 典型产出 |
|---|---|---|
| L0 — 无系统 | 每个页面/团队各写各的 | 大量重复代码,视觉不一致 |
| L1 — 样式指南 | 有 Figma 设计规范,但代码没对齐 | 设计规范文档,但开发不遵守 |
| L2 — 组件库 | 有共享的 React 组件包 | npm 包,基础组件可复用 |
| L3 — 设计系统 | Tokens + 组件 + 文档 + 流程 | Storybook、贡献指南、版本管理 |
| L4 — 平台化 | 设计系统作为内部平台运营 | CLI 工具、自动化测试、指标度量 |
大多数团队在 L2 就停下了。从 L2 到 L3 的关键跨越是建立治理机制和文档文化,这往往比技术实现更难。
延展阅读
- Atomic Design — Brad Frost — 组件分层的经典方法论,Atoms/Molecules/Organisms 的原始出处
- Design Tokens W3C Community Group — DTCG 规范官方页面,2025.10 稳定版
- Style Dictionary — Amazon 开源的 token 编译工具,支持 DTCG 格式
- Radix UI — 最流行的 headless React 组件库,shadcn/ui 的基础层
- React Aria — Adobe — Adobe 的 headless hooks 库,a11y 合规性最强
- shadcn/ui — "拥有代码"范式的代表,Radix + Tailwind 的最佳实践集合
- Design Systems Handbook — InVision — 设计系统建设的全面指南
- Subatomic Design Tokens Course — Brad Frost — Brad Frost 的 Design Tokens 深度课程