Skip to content

设计系统与组件库:从规范到落地 ​

很多团队都有"组件库",但真正能长期运转的设计系统很少。差别不在组件数量,而在于是否解决了三个问题:设计决策如何统一、组件 API 如何稳定、多人协作如何持续。

这篇文章从 Token、组件设计、文档和治理四个方面,讲一套可以落地的设计系统方法。

Token:把设计决策变成变量 ​

Design Token 是设计系统的最小单元:颜色、间距、字号、圆角、阴影、动效时长。它的价值不是"少写几个色值",而是让设计决策可以被统一修改。

Token 通常分三层:

text
Primitive(原始值)   →  indigo-500: #6366f1
Semantic(语义)      →  action-primary: var(--color-indigo-500)
Component(组件级)   →  button-bg: var(--action-primary)
  • Primitive 只描述数值,不承载业务含义。
  • Semantic 描述用途,例如 text-primary、bg-surface、border-danger。
  • Component 只在必要时使用,避免组件层过度耦合。

业务组件应该只消费 Semantic Token,不直接引用 Primitive:

css
:root {
  --color-indigo-500: #6366f1;
  --color-slate-900: #0f172a;
  --color-white: #ffffff;

  --color-bg-canvas: var(--color-white);
  --color-text-primary: var(--color-slate-900);
  --color-action-primary: var(--color-indigo-500);
  --color-border-subtle: rgba(15, 23, 42, 0.12);
}

[data-theme='dark'] {
  --color-bg-canvas: #0b1120;
  --color-text-primary: #e2e8f0;
  --color-border-subtle: rgba(226, 232, 240, 0.14);
}

这样切换主题只需要覆盖 Semantic Token,不需要改组件样式。

Token 的源头建议放在独立文件中(JSON 或 YAML),通过 Style Dictionary 等工具生成 CSS 变量、TypeScript 类型和 Figma 变量,避免设计稿与代码各维护一份。

组件 API 设计 ​

组件库最常见的失败模式是"props 爆炸":一个 Button 支持十几种变体、二十个布尔值,最终没人能记住组合规则。

用有限枚举表达变体 ​

vue
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  loading?: boolean
  disabled?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  loading: false,
  disabled: false
})

const emit = defineEmits<{ click: [event: MouseEvent] }>()

function handleClick(event: MouseEvent) {
  if (props.loading || props.disabled) return
  emit('click', event)
}
</script>

<template>
  <button
    :class="['btn', `btn--${props.variant}`, `btn--${props.size}`]"
    :disabled="props.disabled || props.loading"
    :aria-busy="props.loading"
    type="button"
    @click="handleClick"
  >
    <span v-if="props.loading" class="btn__spinner" aria-hidden="true" />
    <slot />
  </button>
</template>

变体用枚举,不用多个布尔值;禁用和加载状态要同时处理可访问性,aria-busy 和 aria-disabled 比单纯改颜色更有意义。

受控与非受控 ​

输入类组件应同时支持受控和非受控模式:

tsx
interface InputProps {
  value?: string
  defaultValue?: string
  onChange?: (value: string) => void
}
  • 传入 value 时由外部控制,组件只触发 onChange。
  • 只传 defaultValue 时组件内部维护状态。
  • 不要在两种模式间来回切换,行为会变得不可预测。

组合优于配置 ​

复杂组件优先提供组合能力,而不是不断增加 props:

tsx
<Select value={value} onChange={setValue}>
  <Select.Trigger>请选择</Select.Trigger>
  <Select.Content>
    <Select.Item value="web">Web</Select.Item>
    <Select.Item value="app">App</Select.Item>
  </Select.Content>
</Select>

这种复合组件模式让调用方控制结构,库负责状态、键盘交互和无障碍属性。Headless UI 库(如 Radix、Headless UI、Ark UI)就是这一思路的典型实现:行为逻辑与视觉样式分离。

无障碍不是可选项 ​

组件库必须把基础无障碍能力内置,否则每个业务团队都会重复犯错:

  • 所有交互元素可通过键盘访问,焦点状态可见。
  • 弹窗、下拉菜单、Tabs 遵循 ARIA 模式,焦点管理正确。
  • 表单控件有关联的 label、错误提示使用 aria-describedby。
  • 颜色对比度满足 WCAG AA。
  • 动效尊重 prefers-reduced-motion。
css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

组件库的一个错误会影响所有业务,无障碍规范应在组件层解决,而不是交给使用方。

文档与示例 ​

组件没有文档,等于没有组件。文档至少包含:

  • 使用场景与不适用场景。
  • 可运行的示例代码。
  • 完整的 props / events / slots 表格。
  • 无障碍说明。
  • 设计稿与 Token 对应关系。
  • 变更记录和迁移指南。

Storybook 适合承载组件开发与文档,每个组件维护 Story:

tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'

const meta = {
  title: 'Components/Button',
  component: Button,
  args: { children: '提交' }
} satisfies Meta<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

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

export const Loading: Story = {
  args: { loading: true }
}

视觉回归测试可以在 Storybook 基础上用 Playwright 截图对比,防止样式意外变更。

版本与发布 ​

组件库是公共依赖,破坏性变更的成本很高。发布策略:

  • 遵循 SemVer。删除或修改 props、事件、DOM 结构都可能是 breaking change。
  • 提供 codemod 或迁移文档,不要让使用方自己猜。
  • 废弃 API 先标记 deprecated,保留至少一个大版本。
  • 每个版本生成 changelog,关联设计和开发变更。
  • 包体积和 Tree Shaking 纳入 CI 检查。
json
{
  "sideEffects": ["**/*.css"],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./styles.css": "./dist/styles.css"
  }
}

sideEffects 声明错误会导致 Tree Shaking 失效,或者把必要的样式摇掉,这是组件库打包的高频问题。

治理机制 ​

设计系统不是"设计团队交付一套组件,前端照着实现",而是持续协作:

  1. 需求入口统一:业务提出需求,先判断是通用能力还是业务特例。
  2. 设计评审与开发评审:新组件同时评审交互、视觉、API 和无障碍。
  3. 贡献流程:外部团队可以提交组件,但需遵循规范并有维护者 review。
  4. 采用率度量:统计业务项目使用设计系统的比例、组件重复率、样式覆盖次数。
  5. 定期清理:删除无人使用的组件和 Token,避免系统膨胀。

常见坑 ​

  1. Token 只有一套颜色。没有语义层,换主题或做多品牌时只能全局替换。
  2. 组件追求大而全。一个组件支持所有业务场景,最终无法维护。
  3. 业务样式直接覆盖组件内部结构。组件 DOM 变成隐式 API,改结构就破坏业务。
  4. 无障碍后补。等组件被大量使用后再改,迁移成本极高。
  5. 文档与实现脱节。示例不随版本更新,使用方被过期文档误导。
  6. 没有 deprecation 流程。破坏性变更直接发布,业务团队被迫紧急修复。

小结 ​

设计系统由 Token、组件、文档和治理四部分组成。Token 统一设计决策,组件封装交互与无障碍,文档降低使用成本,治理保证长期演进。组件数量不是目标,让团队用一致的方式构建界面才是。

基于 VitePress 构建 · 持续更新中