设计系统建设

设计系统工程化建设——Design Tokens 体系与 W3C DTCG 规范、组件库架构与发布策略、Storybook 文档驱动开发、多品牌主题方案、治理模型与组织协作、shadcn/ui 范式转变。

设计系统建设

设计系统不是组件库

这是最常见的认知偏差。很多团队说"我们要建设计系统",实际做的是"封装一批 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 是正确的基础层?

  1. 样式自由:消费者可以用 Tailwind、CSS Modules、vanilla CSS,不被组件库的样式方案绑定
  2. 无障碍内建:键盘导航、ARIA 属性、焦点管理这些复杂逻辑由专业团队维护
  3. 行为一致:不同主题/品牌共享同一套交互行为
  4. 减少维护负担:你的团队只需要关注样式层,不需要处理 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 在设计系统中的角色远不止"展示组件"。它是组件开发的主要工作台

  1. 隔离开发:在 Storybook 中开发组件,不需要启动整个应用
  2. 状态覆盖:通过 Stories 覆盖组件的所有状态(loading、error、empty、overflow)
  3. 视觉回归测试:配合 Chromatic 做自动化截图对比
  4. 交互测试:Storybook 8 内置 play function,可以在 story 中编写交互测试
  5. 文档生成:从 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 配置来定制。这个模式的问题:

  1. 定制天花板:props 没暴露的能力就无法定制,除非 fork
  2. 样式覆盖地狱!important、深层选择器覆盖、specificity 战争
  3. 版本升级痛苦:major version 升级经常是大规模 breaking change
  4. bundle size:即使只用 5 个组件,也可能引入整个库的样式

shadcn/ui 的解法

shadcn/ui 不是一个 npm 包,而是一个代码生成器

npx shadcn@latest add button
# → 将 Button 组件的源代码复制到你的项目中
# → 你拥有这份代码,可以任意修改

本质上,shadcn/ui = Radix Primitives(headless 行为层)+ Tailwind CSS(样式层)+ 一套精心设计的默认样式。

这个模式为什么成功?

  1. 完全可控:代码在你的仓库里,想改什么改什么
  2. 零抽象泄漏:没有 theme provider、没有 CSS-in-JS runtime、没有样式覆盖问题
  3. 渐进式采用:需要哪个组件就加哪个,不是 all-or-nothing
  4. 学习价值:可以直接阅读组件源码,理解最佳实践

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 的关键跨越是建立治理机制和文档文化,这往往比技术实现更难。


延展阅读