设计系统与组件库

What — 什么是设计系统

设计系统(Design System)是一套完整的设计标准和组件集合,包含设计原则、设计令牌、组件库、模式库和文档。它确保产品在不同页面、不同团队、不同平台间保持视觉和交互一致性。

设计系统的层次

设计系统
├── 设计原则(Design Principles)  — 为什么这样设计
├── 设计令牌(Design Tokens)     — 最小的设计决策单元
├── 组件库(Component Library)   — 可复用的 UI 组件
├── 模式库(Pattern Library)     — 组件的组合模式
└── 文档(Documentation)         — 使用指南和最佳实践

设计令牌(Design Tokens)

设计令牌是设计系统的原子单位——一个颜色值、一个间距值、一个字号,都是一个 Token。

// design-tokens.json
{
  "color": {
    "primary":   { "value": "#3b82f6" },
    "secondary": { "value": "#6b7280" },
    "success":   { "value": "#10b981" },
    "danger":    { "value": "#ef4444" },
    "warning":   { "value": "#f59e0b" },
    "text": {
      "primary":   { "value": "#1f2937" },
      "secondary": { "value": "#6b7280" },
      "disabled":  { "value": "#9ca3af" }
    },
    "background": {
      "primary":   { "value": "#ffffff" },
      "secondary": { "value": "#f9fafb" },
      "elevated":  { "value": "#ffffff" }
    }
  },
  "spacing": {
    "xs": { "value": "4px" },
    "sm": { "value": "8px" },
    "md": { "value": "16px" },
    "lg": { "value": "24px" },
    "xl": { "value": "32px" }
  },
  "typography": {
    "fontSize": {
      "xs": { "value": "12px" },
      "sm": { "value": "14px" },
      "base": { "value": "16px" },
      "lg": { "value": "18px" },
      "xl": { "value": "20px" },
      "2xl": { "value": "24px" }
    },
    "fontWeight": {
      "normal": { "value": "400" },
      "medium": { "value": "500" },
      "semibold": { "value": "600" },
      "bold": { "value": "700" }
    },
    "lineHeight": {
      "tight": { "value": "1.25" },
      "normal": { "value": "1.5" },
      "relaxed": { "value": "1.75" }
    }
  },
  "borderRadius": {
    "sm": { "value": "4px" },
    "md": { "value": "8px" },
    "lg": { "value": "12px" },
    "full": { "value": "9999px" }
  },
  "shadow": {
    "sm": { "value": "0 1px 2px rgba(0,0,0,0.05)" },
    "md": { "value": "0 4px 6px rgba(0,0,0,0.1)" },
    "lg": { "value": "0 10px 15px rgba(0,0,0,0.1)" }
  }
}

Token 的三层架构

层级说明示例
Global Tokens全局原始值blue-500: #3b82f6
Alias Tokens语义化别名color-primary: {blue-500}
Component Tokens组件级变量button-bg-primary: {color-primary}
/* Global → Alias → Component */
:root {
  /* Global */
  --blue-500: #3b82f6;
  --gray-100: #f3f4f6;
  --spacing-md: 16px;

  /* Alias */
  --color-primary: var(--blue-500);
  --color-surface: var(--gray-100);
  --spacing-comfortable: var(--spacing-md);

  /* Component */
  --button-bg: var(--color-primary);
  --button-padding: var(--spacing-comfortable);
  --card-surface: var(--color-surface);
}

Why — 为什么需要设计系统

1. 一致性

没有设计系统时,不同开发者可能用不同的蓝色(#3b82f6 vs #2563eb vs #4f86f7)、不同的间距(12px vs 14px vs 16px),产品视觉碎片化。

2. 效率

设计系统提供预制组件,开发者无需从零构建每个按钮、表单、弹窗。统计显示,使用设计系统可以减少 30-50% 的前端开发时间。

3. 协作

设计系统和开发共享同一套 Token,设计师在 Figma 中用的颜色值和开发在代码中用的完全一致,消除”设计稿与实现不一致”的问题。

4. 可扩展性

新增主题(暗色模式、品牌定制)只需修改 Token 值,无需修改组件代码。

对比有无设计系统

维度无设计系统有设计系统
视觉一致性
开发速度慢(重复造轮子)快(组件复用)
设计-开发协作反复对齐共享 Token
主题扩展逐个修改改 Token 全局生效
新人上手快(文档 + 组件)
维护成本高(分散修改)低(集中管理)

How — 怎么构建

1. 从 Token 开始

npm install style-dictionary
// style-dictionary.config.js
module.exports = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'dist/',
      files: [{
        destination: 'tokens.css',
        format: 'css/variables',
        options: { outputReferences: true }
      }]
    },
    scss: {
      transformGroup: 'scss',
      buildPath: 'dist/',
      files: [{
        destination: 'tokens.scss',
        format: 'scss/variables',
        options: { outputReferences: true }
      }]
    },
    js: {
      transformGroup: 'js',
      buildPath: 'dist/',
      files: [{
        destination: 'tokens.js',
        format: 'javascript/es6'
      }]
    }
  }
}
npx style-dictionary build

产物:

/* dist/tokens.css */
:root {
  --color-primary: #3b82f6;
  --color-secondary: #6b7280;
  --spacing-md: 16px;
  --font-size-base: 16px;
  --border-radius-md: 8px;
  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
}

2. 组件库架构

my-ui/
├── packages/
│   ├── tokens/          — 设计令牌
│   ├── core/            — 基础工具(合并类名、主题上下文)
│   ├── components/      — 组件源码
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   └── index.ts
│   │   ├── Input/
│   │   ├── Modal/
│   │   └── index.ts     — 统一导出
│   └── docs/            — 文档站点
├── scripts/
├── package.json
└── tsconfig.json

3. 组件设计原则

API 设计——变体(Variants)

// 使用 CVA (Class Variance Authority) 管理变体
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  // 基础样式
  'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2',
  {
    variants: {
      variant: {
        primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-300',
        secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-300',
        outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-300',
        danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-300',
        ghost: 'text-gray-600 hover:bg-gray-100 focus:ring-gray-300',
      },
      size: {
        sm: 'h-8 px-3 text-sm rounded',
        md: 'h-10 px-4 text-sm rounded-md',
        lg: 'h-12 px-6 text-base rounded-lg',
      },
      fullWidth: {
        true: 'w-full',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean
}

function Button({ variant, size, fullWidth, loading, className, children, ...props }: ButtonProps) {
  return (
    <button
      className={buttonVariants({ variant, size, fullWidth, className })}
      disabled={loading || props.disabled}
      {...props}
    >
      {loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
      {children}
    </button>
  )
}

Composition 模式——组件组合

// ❌ 过度配置化:所有功能塞进一个组件
<Modal
  title="Confirm"
  body="Are you sure?"
  showCloseButton
  closeOnOverlay
  footer={<Button>OK</Button>}
/>

// ✅ 组合模式:各部分独立
<Modal>
  <Modal.Header>
    <Modal.Title>Confirm</Modal.Title>
    <Modal.CloseButton />
  </Modal.Header>
  <Modal.Body>Are you sure?</Modal.Body>
  <Modal.Footer>
    <Button variant="secondary">Cancel</Button>
    <Button variant="primary">Confirm</Button>
  </Modal.Footer>
</Modal>

4. 主题系统

// ThemeProvider
const defaultTheme = {
  colors: {
    primary: 'var(--color-primary)',
    secondary: 'var(--color-secondary)',
    success: 'var(--color-success)',
    danger: 'var(--color-danger)',
    text: 'var(--color-text-primary)',
    background: 'var(--color-background-primary)',
  },
  spacing: {
    xs: 'var(--spacing-xs)',
    sm: 'var(--spacing-sm)',
    md: 'var(--spacing-md)',
    lg: 'var(--spacing-lg)',
    xl: 'var(--spacing-xl)',
  },
  borderRadius: {
    sm: 'var(--border-radius-sm)',
    md: 'var(--border-radius-md)',
    lg: 'var(--border-radius-lg)',
  },
}

const ThemeContext = createContext(defaultTheme)

function ThemeProvider({ theme, children }) {
  return (
    <ThemeContext.Provider value={{ ...defaultTheme, ...theme }}>
      {children}
    </ThemeContext.Provider>
  )
}

function useTheme() {
  return useContext(ThemeContext)
}

暗色模式

/* tokens.css */
:root {
  --color-primary: #3b82f6;
  --color-text-primary: #1f2937;
  --color-background-primary: #ffffff;
}

[data-theme="dark"] {
  --color-primary: #60a5fa;
  --color-text-primary: #f3f4f6;
  --color-background-primary: #1f2937;
}
function ThemeSwitcher() {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme)
  }, [theme])

  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      {theme === 'light' ? 'Dark' : 'Light'}
    </button>
  )
}

5. 无障碍(Accessibility)

function Dialog({ open, onClose, title, children }) {
  const dialogRef = useRef<HTMLDialogElement>(null)

  useEffect(() => {
    if (open) {
      dialogRef.current?.showModal()
    } else {
      dialogRef.current?.close()
    }
  }, [open])

  return (
    <dialog
      ref={dialogRef}
      onClose={onClose}
      aria-labelledby="dialog-title"
      aria-modal="true"
      className="rounded-lg p-0 shadow-lg"
    >
      <div className="p-6">
        <h2 id="dialog-title" className="text-lg font-semibold">{title}</h2>
        <div className="mt-4">{children}</div>
      </div>
    </dialog>
  )
}

组件库的 A11y 清单

检查项说明
键盘导航所有交互可用 Tab/Enter/Space/Escape 操作
ARIA 属性正确使用 role、aria-label、aria-describedby
焦点管理弹窗打开时聚焦到弹窗,关闭后回到触发元素
颜色对比度文字与背景对比度 ≥ 4.5:1(WCAG AA)
屏幕阅读器使用 VoiceOver/NVDA 测试
减少动画prefers-reduced-motion 时禁用动画

6. 文档与 Storybook

npx storybook@latest init
// 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', 'outline', 'danger', 'ghost'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
  },
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: { variant: 'primary', children: 'Primary' },
}

export const AllVariants: Story = {
  render: () => (
    <div className="flex gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="danger">Danger</Button>
      <Button variant="ghost">Ghost</Button>
    </div>
  ),
}

7. 主流组件库对比

维度Ant DesignElement PlusRadix UIHeadless UIshadcn/ui
框架ReactVueReactReact/VueReact
样式CSS-in-JSSCSS无样式无样式Tailwind
定制性极高极高
开箱即用
体积大(~600KB)中(~300KB)小(按需)小(按需)小(按需)
A11y一般优秀优秀优秀
设计风格企业级简约简约现代

shadcn/ui 的独特模式

shadcn/ui 不是 npm 包,而是 CLI 工具,直接将组件源码复制到你的项目中,你完全拥有代码,可以随意修改。

npx shadcn@latest init
npx shadcn@latest add button dialog table

常见问题与踩坑

问题原因解决方案
设计与开发不一致设计师和开发用不同的色值共享 Design Token,Figma 插件同步
组件定制困难组件内部样式不可覆盖使用 CSS 变量 + ::part() 暴露钩子
体积过大引入整个组件库按需导入(Tree Shaking)
版本升级破坏组件 API 变更语义化版本 + 迁移指南
多框架重复开发同一设计不同框架各实现一套用 Web Components 做底层,或用 Headless 方案

最佳实践

  1. Token 先行:先定义设计令牌,再构建组件,确保一致性从底层开始。
  2. Headless 优先:用 Radix UI / Headless UI 处理无障碍和交互逻辑,只写样式。
  3. 组合优于配置:组件用 <Modal.Header> 而非 <Modal headerTitle="...">
  4. CSS 变量做主题:组件内部用 CSS 变量,外部通过覆盖变量定制主题。
  5. 文档即代码:Storybook / Docusaurus 与组件源码同仓库,文档与代码永远同步。

面试题

1. Design Token 是什么?为什么需要三层架构(Global → Alias → Component)?

:Design Token 是设计系统的最小决策单元,代表一个设计值(颜色、间距、字号等)。三层架构的好处:(1) Global Token(如 blue-500: #3b82f6)是原始值,方便维护色板;(2) Alias Token(如 color-primary: {blue-500})赋予语义,修改品牌色只需改别名指向;(3) Component Token(如 button-bg: {color-primary})将组件与语义绑定,主题切换时只需改 Alias 层。没有三层架构的话,改品牌色需要搜索所有 #3b82f6 逐个替换;有了三层架构,只需将 color-primaryblue-500 改为 indigo-500,所有使用该别名的组件自动更新。


2. shadcn/ui 和传统组件库(Ant Design、Element Plus)的核心区别是什么?

:核心区别是分发模式。传统组件库通过 npm 安装,组件代码在 node_modules 中,你只能通过库提供的 API(props、CSS 变量、插槽)定制。shadcn/ui 通过 CLI 将组件源码直接复制到你的项目中,你完全拥有代码,可以随意修改任何细节。优势:定制无上限,不依赖库的 API 设计。劣势:无法通过 npm update 升级,需要手动合并更新。shadcn/ui 本质上不是组件库,而是”组件模板”——它把最佳实践的组件代码给你,剩下的你自己掌控。


3. Headless UI 组件是什么?为什么越来越多组件库采用这个模式?

:Headless UI 组件只提供行为和可访问性逻辑(键盘导航、ARIA 属性、焦点管理),不提供任何样式。开发者负责视觉表现。代表:Radix UI、Headless UI、Ark UI。采用这个模式的原因:(1) 定制性无限——不受库的视觉设计约束,可以做任何风格;(2) 体积更小——只有逻辑代码,无 CSS 运行时;(3) A11y 专业——Headless 库专注处理键盘、屏幕阅读器、焦点等复杂逻辑,开发者无需成为 A11y 专家;(4) 设计系统友好——Headless 做行为,Tailwind/CSS-in-JS 做视觉,分层清晰。


4. 如何实现组件库的按需加载?

:三种方式:(1) Tree Shaking——使用 ES Module 的 named export,构建工具自动摇树优化,前提是 package.json 中配置 "sideEffects": false;(2) 手动按需导入——import Button from 'my-ui/es/button' 直接引入组件目录,而非 import { Button } from 'my-ui';(3) babel-plugin-import——自动将 import { Button } from 'my-ui' 转换为 import Button from 'my-ui/es/button'。现代组件库(支持 ESM + Tree Shaking)通常只需方式(1),无需额外配置。


5. 组件库如何支持多主题(暗色模式、品牌定制)?

:核心是 CSS 变量 + 语义化 Token。组件内部不硬编码颜色值,而是引用 CSS 变量(如 var(--button-bg))。主题切换只需改变量值:(1) 暗色模式——[data-theme="dark"] { --color-primary: #60a5fa; } 覆盖变量;(2) 品牌定制——不同客户加载不同的 Token 文件(brand-a.css vs brand-b.css);(3) 运行时切换——JS 修改 document.documentElement.style.setProperty('--color-primary', newValue)。关键原则:组件只消费 Alias/Component Token,不消费 Global Token,这样改主题不需要改组件代码。


6. 组件库如何保证无障碍性(Accessibility)?

:四个层面保证:(1) 语义化 HTML——用原生 <button> 而非 <div onClick>,用 <dialog> 而非自定义弹窗;(2) ARIA 属性——aria-labelaria-expandedaria-selected 等状态属性自动管理;(3) 键盘导航——Tab 键聚焦、Enter/Space 激活、Escape 关闭弹窗、方向键在列表中移动;(4) 自动化测试——使用 @testing-library/jest-domtoBeVisible()toHaveAttribute() 等 A11y 断言,结合 axe-core 自动检测 WCAG 违规。Headless UI 库(Radix、Headless UI)已经内置了这些逻辑,直接使用即可。


7. 如何评估一个组件库是否适合你的项目?

:六个维度评估:(1) 框架兼容——是否支持你使用的前端框架;(2) 定制能力——是否可以覆盖样式(CSS 变量、::part()、className 合并);(3) 体积影响——打包后增量多少(用 bundlephobia.com 检查);(4) 无障碍——是否有 A11y 测试、ARIA 支持;(5) 社区与维护——GitHub star、issue 响应速度、发布频率;(6) 设计风格匹配——视觉风格是否与产品定位一致。一个常见误区是选了功能最全的库,但 80% 的组件用不到,体积反而成为负担。推荐策略:功能简单选 Headless + Tailwind,企业级后台选 Ant Design / Element Plus。


8. 设计系统如何与 Figma 协同工作?

:Figma 与设计系统通过 Design Token 桥接:(1) Figma Variables——Figma 原生支持设计变量,可以直接映射到代码中的 CSS 变量;(2) Token 同步工具——Tokens Studio(原 Figma Tokens)插件可以在 Figma 中编辑 Token,同步到 JSON 文件,再通过 Style Dictionary 编译为 CSS/JS;(3) Figma API——自动从 Figma 文件提取组件属性和样式,生成代码骨架;(4) Storybook + Figma——Storybook 的 Figma 插件可以直接在文档中嵌入 Figma 设计稿,设计和开发对照查看。工作流:设计师在 Figma 中用 Token 变量设计 → 导出 Token JSON → 开发用 Style Dictionary 编译 → 组件使用编译后的变量。


相关链接