设计系统构建指南
原创
灵阙教研团队
B 基础 入门 |
约 9 分钟阅读
更新于 2026-02-28 AI 导读
设计系统构建指南 组件库架构、Design Tokens、文档化、版本管理、Figma-to-Code 工作流与 Storybook 实践 一、设计系统的本质 设计系统不是组件库,也不是一套 UI 规范文档。它是一套活的、可执行的契约——连接设计师的意图和工程师的实现,确保产品在任何页面、任何设备、任何迭代中都保持一致。 设计系统的三个层次...
设计系统构建指南
组件库架构、Design Tokens、文档化、版本管理、Figma-to-Code 工作流与 Storybook 实践
一、设计系统的本质
设计系统不是组件库,也不是一套 UI 规范文档。它是一套活的、可执行的契约——连接设计师的意图和工程师的实现,确保产品在任何页面、任何设备、任何迭代中都保持一致。
设计系统的三个层次
┌─────────────────────────────────────────────────┐
│ L1: 原则层 (Principles) │
│ 品牌价值观、设计哲学、交互范式 │
├─────────────────────────────────────────────────┤
│ L2: Token 层 (Design Tokens) │
│ 颜色、字体、间距、圆角、阴影、动画曲线 │
├─────────────────────────────────────────────────┤
│ L3: 组件层 (Components) │
│ Button、Input、Card、Modal、Table ... │
└─────────────────────────────────────────────────┘
自建 vs 使用现成方案
| 方案 | 适用场景 | 工作量 | 灵活性 |
|---|---|---|---|
| shadcn/ui | 快速启动、可定制 | 低 | 高(代码在项目中) |
| Ant Design | 企业后台、中文友好 | 低 | 中(主题定制有限) |
| MUI | Material Design 风格 | 低 | 中 |
| Radix + 自定义样式 | 需要完全品牌化 | 中 | 高 |
| 完全自建 | 超大型团队、独特需求 | 高 | 最高 |
中小团队的推荐路径:Radix Primitives(无样式组件)+ Tailwind CSS(样式系统)+ Design Tokens(语义映射)。
二、Design Tokens 架构
2.1 三层 Token 体系
// tokens/primitives.ts - L1: Primitive tokens (raw values)
export const primitives = {
color: {
blue: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
gray: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
// ... green, red, yellow, etc.
},
spacing: {
0: '0px',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
8: '32px',
10: '40px',
12: '48px',
16: '64px',
},
radius: {
none: '0px',
sm: '4px',
md: '8px',
lg: '12px',
xl: '16px',
full: '9999px',
},
shadow: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1)',
},
} as const;
// tokens/semantic.ts - L2: Semantic tokens (design intent)
export const semanticLight = {
color: {
bg: {
primary: primitives.color.gray[50],
secondary: '#ffffff',
inverse: primitives.color.gray[900],
brand: primitives.color.blue[500],
},
text: {
primary: primitives.color.gray[800],
secondary: primitives.color.gray[500],
disabled: primitives.color.gray[300],
inverse: '#ffffff',
brand: primitives.color.blue[600],
},
border: {
default: primitives.color.gray[200],
focus: primitives.color.blue[400],
error: '#ef4444',
},
feedback: {
success: '#22c55e',
warning: '#f59e0b',
error: '#ef4444',
info: primitives.color.blue[500],
},
},
spacing: {
page: primitives.spacing[6],
section: primitives.spacing[8],
element: primitives.spacing[4],
inline: primitives.spacing[2],
},
} as const;
// tokens/component.ts - L3: Component tokens (specific usage)
export const componentTokens = {
button: {
primary: {
bg: semanticLight.color.bg.brand,
text: semanticLight.color.text.inverse,
borderRadius: primitives.radius.md,
paddingX: primitives.spacing[4],
paddingY: primitives.spacing[2],
fontSize: '14px',
fontWeight: '600',
},
secondary: {
bg: 'transparent',
text: semanticLight.color.text.brand,
border: semanticLight.color.border.default,
borderRadius: primitives.radius.md,
paddingX: primitives.spacing[4],
paddingY: primitives.spacing[2],
fontSize: '14px',
fontWeight: '500',
},
},
input: {
bg: '#ffffff',
border: semanticLight.color.border.default,
borderFocus: semanticLight.color.border.focus,
borderError: semanticLight.color.border.error,
borderRadius: primitives.radius.md,
padding: primitives.spacing[3],
fontSize: '14px',
placeholderColor: semanticLight.color.text.disabled,
},
} as const;
2.2 Token 转换为 CSS Variables
// tokens/export-css.ts
function flattenTokens(
obj: Record<string, unknown>,
prefix: string = '',
): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const varName = prefix ? `${prefix}-${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenTokens(value as Record<string, unknown>, varName));
} else {
result[`--${varName}`] = String(value);
}
}
return result;
}
function generateCSS(
lightTokens: Record<string, unknown>,
darkTokens: Record<string, unknown>,
): string {
const lightVars = flattenTokens(lightTokens);
const darkVars = flattenTokens(darkTokens);
const lightCSS = Object.entries(lightVars)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n');
const darkCSS = Object.entries(darkVars)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n');
return `
:root {
${lightCSS}
}
@media (prefers-color-scheme: dark) {
:root {
${darkCSS}
}
}
[data-theme="dark"] {
${darkCSS}
}
`.trim();
}
三、组件库架构
3.1 组件分层
┌────────────────────────────────────────────┐
│ Patterns (业务模式) │
│ LoginForm, DataTable, FileUpload │
├────────────────────────────────────────────┤
│ Components (通用组件) │
│ Button, Input, Select, Dialog, Card │
├────────────────────────────────────────────┤
│ Primitives (基础原语) │
│ Box, Flex, Text, Separator, VisuallyHidden │
└────────────────────────────────────────────┘
3.2 组件规范模板
每个组件必须包含以下结构:
components/
button/
Button.tsx # 组件实现
Button.stories.tsx # Storybook stories
Button.test.tsx # 单元测试
Button.module.css # 样式(或用 Tailwind)
index.ts # 导出入口
README.md # 组件文档(可选)
3.3 组件 API 设计原则
// components/button/Button.tsx
import { forwardRef } from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
/**
* Button component API design principles:
* 1. Variant-driven: visual styles via `variant` prop
* 2. Size tokens: `sm`, `md`, `lg` (not arbitrary px)
* 3. Composition: `asChild` for rendering as different elements
* 4. Accessible: proper ARIA attributes
* 5. Polymorphic: works as button, link, or custom element
*/
const buttonVariants = cva(
// Base styles (always applied)
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary:
'bg-[var(--color-bg-brand)] text-[var(--color-text-inverse)] hover:opacity-90',
secondary:
'border border-[var(--color-border-default)] bg-transparent hover:bg-[var(--color-bg-primary)]',
ghost:
'hover:bg-[var(--color-bg-primary)]',
destructive:
'bg-[var(--color-feedback-error)] text-white hover:opacity-90',
link:
'text-[var(--color-text-brand)] underline-offset-4 hover:underline',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild, loading, children, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={loading || props.disabled}
{...props}
>
{loading ? <Spinner className="mr-2 h-4 w-4" /> : null}
{children}
</Comp>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
export type { ButtonProps };
四、Storybook 集成
4.1 配置
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css'; // Import design tokens
const preview: Preview = {
parameters: {
controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#0f172a' },
{ name: 'gray', value: '#f8fafc' },
],
},
},
decorators: [
(Story) => (
<div className="p-8">
<Story />
</div>
),
],
};
export default preview;
4.2 组件 Story 示例
// components/button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'destructive', 'link'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg', 'icon'],
},
loading: { control: 'boolean' },
disabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { children: 'Primary Button', variant: 'primary' },
};
export const Secondary: Story = {
args: { children: 'Secondary Button', variant: 'secondary' },
};
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4 flex-wrap">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
),
};
export const AllSizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
),
};
export const LoadingState: Story = {
args: { children: 'Loading...', loading: true },
};
五、Figma-to-Code 工作流
5.1 工作流全景
设计师 (Figma)
|
v
[Figma Tokens Studio] -> 导出 tokens.json
|
v
[Style Dictionary / Token Transform] -> 多平台格式
|
v
[Git Commit] -> 触发 CI
|
v
[组件库更新] -> Storybook 自动构建
|
v
[发布 npm 包] -> 项目消费
5.2 Token 同步自动化
// scripts/sync-tokens.ts
/**
* Sync design tokens from Figma Tokens Studio output
* to codebase format.
*
* Run: npx ts-node scripts/sync-tokens.ts
*/
import { readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
interface FigmaToken {
value: string;
type: string;
description?: string;
}
function syncTokens(inputPath: string, outputDir: string): void {
const raw = JSON.parse(readFileSync(inputPath, 'utf-8'));
// Transform Figma Tokens format to our internal format
const primitives = transformPrimitives(raw.global);
const semanticLight = transformSemantic(raw.light, primitives);
const semanticDark = transformSemantic(raw.dark, primitives);
// Write TypeScript files
writeFileSync(
resolve(outputDir, 'primitives.ts'),
`export const primitives = ${JSON.stringify(primitives, null, 2)} as const;\n`
);
writeFileSync(
resolve(outputDir, 'semantic-light.ts'),
`export const semanticLight = ${JSON.stringify(semanticLight, null, 2)} as const;\n`
);
writeFileSync(
resolve(outputDir, 'semantic-dark.ts'),
`export const semanticDark = ${JSON.stringify(semanticDark, null, 2)} as const;\n`
);
// Generate CSS variables
const css = generateCSS(semanticLight, semanticDark);
writeFileSync(resolve(outputDir, 'tokens.css'), css);
console.log(`Synced tokens to ${outputDir}`);
}
六、版本管理与发布
6.1 语义化版本规则
| 变更类型 | 版本变化 | 示例 |
|---|---|---|
| 新增组件 | Minor | 1.2.0 -> 1.3.0 |
| 修复组件 Bug | Patch | 1.2.0 -> 1.2.1 |
| Token 值调整 | Patch | 颜色微调 |
| 破坏性 API 变更 | Major | 删除 prop、改变行为 |
| 新增 Token | Minor | 增加语义色 |
| 删除/重命名 Token | Major | 所有消费者需更新 |
6.2 Changelog 自动化
{
"scripts": {
"release": "changeset publish",
"version": "changeset version",
"storybook:build": "storybook build -o storybook-static",
"storybook:deploy": "gh-pages -d storybook-static",
"tokens:sync": "ts-node scripts/sync-tokens.ts",
"test": "vitest run",
"lint": "eslint . && stylelint '**/*.css'"
}
}
七、设计系统的度量与治理
治理指标
| 指标 | 目标 | 度量方式 |
|---|---|---|
| 组件覆盖率 | > 80% UI 使用设计系统组件 | ESLint 规则检测 |
| Token 使用率 | > 90% 颜色使用 Token | Stylelint 检测硬编码值 |
| 文档完整度 | 100% 组件有 Storybook | CI 检查 story 文件存在 |
| 无障碍合规 | WCAG AA | axe-core 自动扫描 |
| Bundle 影响 | 单组件 < 10KB gzip | Bundle analyzer |
设计系统的生命周期
阶段 1: 基础建设 (1-2 月)
-> Token 体系 + 10-15 核心组件 + Storybook
阶段 2: 推广采用 (2-3 月)
-> 文档完善 + 迁移指南 + 团队培训
阶段 3: 稳定运营 (持续)
-> 需求驱动新增 + Token 同步 + 版本管理
阶段 4: 进化 (按需)
-> 多品牌支持 + 多平台扩展 + AI 辅助
八、经验总结
成功的设计系统具备三个特征
- 易用:开发者用设计系统比不用更快
- 一致:同一组件在任何地方看起来和行为一样
- 活着:持续更新、有人维护、有文档
常见失败原因
- 过度设计:一开始就想覆盖所有场景,结果什么都做不好
- 与业务脱节:设计系统团队闭门造车,产出的组件业务不需要
- 缺少治理:没有强制使用的机制,逐渐被绕过
- Token 漂移:设计稿和代码中的 Token 不同步
设计系统的核心价值不是"统一",而是"减速"——减少每次 UI 决策的认知负荷,让团队把精力放在真正的产品问题上。
Maurice | maurice_wen@proton.me