用 Claude Code 搭建设计系统:Design Tokens、Storybook 与 CI 实战
从设计令牌到 Storybook、无障碍与 CI,学习如何让 Claude Code 安全地协助搭建设计系统。
先别急着做按钮:设计系统最容易失败在“改不动”
团队第一次搭建设计系统时,常见做法是先做一批看起来整齐的按钮、卡片和表单。几周后,品牌色要调整、禁用状态要统一、焦点样式要补齐,大家才发现同一个颜色散落在几十个文件里,Storybook 也没有覆盖真实状态。组件虽然存在,却没有一套可以持续修改和验证的工作方式。
设计系统不是组件陈列室,而是一套运营模型:颜色、间距、字体、状态、评审和测试如何从一个决定稳定地传到产品界面。Claude Code 能读取现有代码、跨文件修改、执行 Storybook 和测试并汇报差异,因此适合处理边界清楚、结果可验证的工作。但品牌含义、公共 API、最终无障碍体验和是否接受视觉差异,仍要由人判断。
本文从一个最小但完整的示例出发,连接 tokens.json、React/TypeScript 组件、Storybook、无障碍检查、视觉回归和 CI。你不需要一次重写整个前端;先挑一个按钮和一组令牌,就能建立可重复的迁移路径。
本文要解决的四件事
- 用 primitive、semantic、component 三层令牌避免颜色值散落在组件中。
- 让 Claude Code 只改指定目录,并在提交前留下可复核的测试结果。
- 采用 2026 年推荐的
@storybook/addon-vitest与vitest --project=storybook,同时保留必要的人工无障碍评审。 - 把 Figma 当作评审输入,而不是一开始就进行有风险的双向自动同步。
延伸阅读可参考用 Claude Code 管理设计令牌、用 Claude Code 开发 Storybook与用 Claude Code 改善无障碍体验。
目标架构:让 tokens.json 成为代码侧契约
这套流程以 tokens.json 为代码侧唯一可审查的事实来源。Figma 对设计工作仍然不可替代,但进入代码和 CI 的变更必须变成可比较、可回滚的契约。
flowchart LR
Figma["Figma Variables"]
Tokens["tokens.json"]
Build["token build script"]
CSS["CSS variables"]
TS["TypeScript token map"]
Components["React components"]
Storybook["Storybook stories"]
CI["Visual and a11y CI"]
Figma -->|review input| Tokens
Tokens --> Build
Build --> CSS
Build --> TS
CSS --> Components
TS --> Components
Components --> Storybook
Storybook --> CI
Design Token(设计令牌)就是把设计决定以有名字的数据保存下来,例如颜色、间距、圆角、字体和组件状态。组件里直接写 #2563eb 只说明“它是蓝色”;使用 action.background.primary 之类的语义令牌,才说明“它是主要操作的背景色”。未来换品牌色时,修改令牌即可,不必在组件里搜索替换。
当前规范与工具请以官方资料为准:Design Tokens Community Group 格式、Claude Code 文档、Claude Code 安全指南、Storybook Vitest addon、Storybook 无障碍测试、Storybook 视觉测试与 Figma REST API。
Claude Code 负责什么,人负责什么
“帮我做一个设计系统”没有文件范围、验收条件和禁止事项,往往会产生难以评审的大改动。更安全的任务是:“只迁移 Button,保持公共 API,补齐 Storybook 状态,执行无障碍测试,并列出差异。”
| 工作领域 | 适合交给 Claude Code | 必须由人决定 |
|---|---|---|
| 令牌 | 从 CSS 找出重复颜色与间距,生成候选清单 | 品牌含义与令牌命名 |
| 组件 | 实现有类型的 Button、Input、Alert 基础组件 | 公共 API 与产品语义 |
| Storybook | 增加变体、状态与交互 story | 哪些状态对应真实业务流程 |
| 无障碍 | 找出缺失标签、焦点问题与 axe 违规 | 最终键盘、读屏与体验判断 |
| CI | 在 Pull Request 中加入视觉与无障碍检查 | 失败策略与例外审批流程 |
编辑前先把项目规则交给 Claude Code:
设计系统任务规则:
- 只允许编辑 src/components、src/styles、.storybook、tests、scripts 和 tokens.json。
- 修改品牌色前,必须列出旧令牌名与新令牌名。
- 每个新组件都要有 TypeScript props、键盘行为、Storybook stories 和无障碍说明。
- 汇报完成前,运行 npm run tokens:build、npm run test:storybook -- --run、npm run build-storybook 和 npm run test:visual。
- 如果焦点行为发生变化,附上人工检查步骤。
安全边界同样属于设计系统工程。不要把 Figma token、npm token、CI 密钥或客户私有截图粘贴到提示词中。Claude Code 权限应限制在任务需要的目录和命令;执行前检查命令;大批量 snapshot 更新必须由人确认。
最小安装:2026 年优先使用 Storybook Vitest addon
下面假设项目使用 React、TypeScript 和 utility class。使用 pnpm 或 yarn 时,请改成对应命令。
npm install class-variance-authority clsx tailwind-merge
npx storybook@latest init
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D @playwright/test concurrently http-server wait-on
npx playwright install chromium
Vitest addon 需要基于 Vite 的 Storybook framework,或受支持的 Next.js Vite 集成。若旧项目暂时无法满足该条件,再参考 Storybook 官方迁移资料,把旧 @storybook/test-runner 作为兼容回退方案;不要把旧方案当作新项目的推荐配置。
在 package.json 中加入本地和 CI 共用的脚本。关键点是 test:storybook 使用 vitest --project=storybook:
{
"scripts": {
"tokens:build": "node scripts/build-tokens.mjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test:storybook": "vitest --project=storybook",
"test:visual": "playwright test tests/button.visual.spec.ts"
}
}
用三层 Design Token 建立变更契约
将令牌分为 primitive、semantic、component 三层。Primitive 保存原始值;semantic 说明用途;component 表示某个组件的具体状态。这样既能保留基础色板,也能避免组件直接依赖颜色编号。
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"primitive": {
"color": {
"blue": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9373, 0.9647, 1], "hex": "#eff6ff" }
},
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1451, 0.3882, 0.9216], "hex": "#2563eb" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1137, 0.3059, 0.8471], "hex": "#1d4ed8" }
}
},
"gray": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9765, 0.9804, 0.9843], "hex": "#f9fafb" }
},
"200": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.898, 0.9059, 0.9216], "hex": "#e5e7eb" }
},
"900": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.0667, 0.0941, 0.1529], "hex": "#111827" }
}
},
"red": {
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.8627, 0.149, 0.149], "hex": "#dc2626" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.7255, 0.1098, 0.1098], "hex": "#b91c1c" }
}
},
"white": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }
}
},
"space": {
"2": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } },
"3": { "$type": "dimension", "$value": { "value": 0.75, "unit": "rem" } },
"4": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } },
"6": { "$type": "dimension", "$value": { "value": 1.5, "unit": "rem" } }
},
"radius": {
"md": { "$type": "dimension", "$value": { "value": 0.375, "unit": "rem" } },
"lg": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } }
}
},
"semantic": {
"color": {
"surface": { "$type": "color", "$value": "{primitive.color.white}" },
"text": { "$type": "color", "$value": "{primitive.color.gray.900}" },
"border": { "$type": "color", "$value": "{primitive.color.gray.200}" },
"focus": { "$type": "color", "$value": "{primitive.color.blue.600}" }
}
},
"component": {
"button": {
"primary": {
"background": { "$type": "color", "$value": "{primitive.color.blue.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.blue.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
},
"danger": {
"background": { "$type": "color", "$value": "{primitive.color.red.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.red.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
}
}
}
}
再从同一文件生成 CSS 变量和 TypeScript token map,避免人工维护两份数据:
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
const source = JSON.parse(readFileSync("tokens.json", "utf8"));
function getToken(path) {
const node = path.split(".").reduce((current, key) => current?.[key], source);
if (!node || typeof node.$value === "undefined") {
throw new Error(`Unknown token reference: ${path}`);
}
return node.$value;
}
function resolveValue(value, stack = []) {
if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
const path = value.slice(1, -1);
if (stack.includes(path)) {
throw new Error(`Circular token reference: ${[...stack, path].join(" -> ")}`);
}
return resolveValue(getToken(path), [...stack, path]);
}
return value;
}
function toCssValue(value) {
if (value && typeof value === "object") {
if (typeof value.hex === "string") return value.hex;
if (typeof value.value === "number" && typeof value.unit === "string") {
return `${value.value}${value.unit}`;
}
throw new Error(`Unsupported token value: ${JSON.stringify(value)}`);
}
return String(value);
}
function walk(node, pathParts = [], result = {}) {
if (!node || typeof node !== "object") return result;
if (node && typeof node === "object" && typeof node.$value !== "undefined") {
result[pathParts.join("-")] = toCssValue(resolveValue(node.$value));
return result;
}
for (const [key, value] of Object.entries(node)) {
if (key.startsWith("$")) continue;
walk(value, [...pathParts, key], result);
}
return result;
}
const flat = walk(source);
const css = [
":root {",
...Object.entries(flat).map(([name, value]) => ` --${name}: ${value};`),
"}",
""
].join("\n");
mkdirSync(dirname("src/styles/tokens.css"), { recursive: true });
mkdirSync(dirname("src/tokens.ts"), { recursive: true });
writeFileSync("src/styles/tokens.css", css);
writeFileSync("src/tokens.ts", `export const tokens = ${JSON.stringify(flat, null, 2)} as const;\n`);
console.log(`Generated ${Object.keys(flat).length} tokens.`);
第一步不要让 Claude Code 重写所有 UI。先让它“找出重复的原始颜色和间距,映射成候选令牌,只输出报告,不编辑文件”。人确认命名和范围后,再迁移一个组件。
构建类型稳定的 React 组件
组件层应当朴素、可预测。下面的 Button 包含变体、尺寸、loading、disabled 和清晰的焦点样式,同时保留原生 button 属性。
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2 rounded-md font-medium",
"transition-colors focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-[var(--semantic-color-focus)] focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50"
],
{
variants: {
variant: {
primary: [
"bg-[var(--component-button-primary-background)]",
"text-[var(--component-button-primary-text)]",
"hover:bg-[var(--component-button-primary-backgroundHover)]"
],
secondary: "border border-[var(--semantic-color-border)] bg-[var(--semantic-color-surface)] text-[var(--semantic-color-text)] hover:bg-gray-50",
danger: [
"bg-[var(--component-button-danger-background)]",
"text-[var(--component-button-danger-text)]",
"hover:bg-[var(--component-button-danger-backgroundHover)]"
]
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base"
}
},
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, variant, size, loading = false, disabled, children, ...props },
ref
) {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading ? (
<span
aria-hidden="true"
className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
/>
) : null}
<span>{children}</span>
</button>
);
});
评审时不要只问“按钮好不好看”。更重要的问题是:这个 API 是否稳定到可以被多个产品团队长期使用?loading 时是否真的阻止重复提交?新增变体会不会破坏已有调用?这些判断需要人结合产品场景完成。
把 Storybook 当作可执行规格
所有重要状态都应有对应 story。Storybook 中不存在的状态,很难被设计师评审、被测试捕获,也难以在团队讨论中准确引用。
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
title: "Design System/Button",
component: Button,
parameters: {
layout: "centered",
a11y: {
test: "error"
}
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "danger"]
},
size: {
control: "select",
options: ["sm", "md", "lg"]
},
loading: { control: "boolean" },
disabled: { control: "boolean" }
}
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: "Save changes",
variant: "primary"
}
};
export const Danger: Story = {
args: {
children: "Delete",
variant: "danger"
}
};
export const Loading: Story = {
args: {
children: "Saving",
loading: true
}
};
export const AllStates: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" size="lg">Large</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
</div>
)
};
提示 Claude Code 保留现有 stories、只补缺少的状态,并解释任何 story ID 变化。这样视觉 snapshot 和无障碍报告仍能对应到明确的评审对象。
在 CI 中执行组件、无障碍与视觉检查
自动无障碍检查不能代替键盘和读屏器人工测试,但能提早发现不少结构性问题。对基于 Vite 的 Storybook,2026 年推荐路径是 @storybook/addon-vitest。它把 stories 转为浏览器测试;当 story 配置 parameters.a11y.test = "error" 时,无障碍 addon 报告违规会直接让组件测试失败。
CI 中执行 npm run test:storybook -- --run。与旧 @storybook/test-runner 流程不同,Vitest addon 在组件和无障碍测试时不需要另外启动一个 Storybook 服务。下方自定义 Playwright 截图仍需要构建并启动 Storybook。
视觉测试先覆盖高价值状态,不要一开始为所有 story 建 snapshot:
启用 CI 前,先在目标应用中执行一次 npx playwright test tests/button.visual.spec.ts --update-snapshots,由人工检查基准图并提交到仓库。没有基准图时,Playwright 无法比较,首次 CI 会失败。
import { expect, test } from "@playwright/test";
test("button all states visual snapshot", async ({ page }) => {
await page.goto("http://127.0.0.1:6006/iframe.html?id=design-system-button--all-states");
await expect(page).toHaveScreenshot("button-all-states.png", {
fullPage: true,
animations: "disabled"
});
});
再把令牌、组件、Storybook 与截图检查接入 GitHub Actions:
name: design-system-quality
on:
pull_request:
paths:
- "tokens.json"
- "scripts/build-tokens.mjs"
- "src/components/**"
- "src/styles/**"
- ".storybook/**"
- "tests/**"
- "package.json"
- "package-lock.json"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run tokens:build
- run: npx playwright install --with-deps chromium
- run: npm run test:storybook -- --run
- run: npm run build-storybook
- run: >
npx concurrently -k -s first -n server,tests
"npx http-server storybook-static -p 6006"
"npx wait-on http://127.0.0.1:6006 && npm run test:visual"
CI 失败时,把失败的 story ID、axe 违规、变更文件和视觉差异交给 Claude Code。不要把密钥或整份可能包含私密信息的日志直接贴进提示词。
Figma 集成的现实边界
Figma Variables 是令牌工作的有力输入,但初期就做自动双向同步风险很高。未审批的试验、旧组件名和私有设计备注,都可能被意外带入生产令牌。
| 对象 | 适合自动化的工作 | 应避免的工作 |
|---|---|---|
| Figma Variables | 导出后与 tokens.json 比较 | 直接覆盖生产令牌 |
| Figma Components | 收集状态与 prop 候选 | 自动决定 React API |
| Figma comments | 汇总尚未解决的问题 | 猜测最终设计意图 |
| Storybook links | 把 story URL 附在设计评审中 | 把 Storybook 当作设计审批 |
先让 Claude Code 生成只读差异报告:
读取 figma-tokens-export.json 和 tokens.json。
生成一份 Markdown 报告,列出:
1. Figma 中存在但代码中不存在的令牌
2. 代码中存在但 Figma 中不存在的令牌
3. 同名语义令牌的值差异
不要编辑 tokens.json,也不要重命名令牌。标记 focus、danger 与 text color 相关的高风险差异。
目标不是为了同步而同步,而是得到一份人能安全评审的差异。团队尚未明确事实来源时,把 Figma 集成保持在 report-only 模式。
Use case 1:SaaS 管理后台逐屏迁移
SaaS 后台的按钮、表单、表格和弹窗通常有大量 loading、disabled、error 与权限状态。先让 Claude Code 统计现有 Button 用法,找出不兼容的 props,再建立兼容层并迁移一个页面。人负责确认删除、付款、权限变更等高风险操作的文案与交互。
验收标准应包括旧页面未变化、新页面全部状态进入 Storybook、键盘操作可完成,以及 token build 和 CI 都通过。不要把几十个页面塞进同一个 Pull Request。
Use case 2:白标产品的品牌切换
白标产品会为不同客户更换基础品牌色,但“主要操作”“危险操作”“正文文字”等语义应保持稳定。Claude Code 可以读取各品牌 primitive token,生成对应 CSS variables,并为 Storybook 增加主题切换器。
人需要确认颜色对比度和品牌许可,尤其是 focus 与 danger 颜色。不能因为客户给了一个品牌蓝,就自动认定它适合按钮文字或焦点环。
Use case 3:旧 CSS 的渐进式清理
旧项目常有多个近似蓝色、不同间距和重复圆角。Claude Code 可扫描 raw value,按出现次数和上下文分组,再输出“原始值 → 候选令牌 → 使用文件”的迁移表。
先替换一个组件并对比视觉 snapshot。若一次提交替换全站,颜色变化来自令牌错误还是组件例外将很难判断。每批迁移都保留回滚点。
Use case 4:营销页与咨询漏斗
咨询站点的 CTA 按钮、价格卡片和表单状态若不一致,会降低访客信任,也让 A/B 测试难以解释。Claude Code 可以统一这些组件并补齐提交中、提交失败、成功与重复点击状态。
转化策略仍由人决定。设计系统只保证实验使用相同组件和可追踪状态,不会自动证明某个文案或颜色带来更多咨询。
常见陷阱:原因与修正方法
陷阱 1:组件直接使用 primitive token
原因: blue-600 看起来方便,却把品牌外观写进组件依赖。品牌变化会变成全仓库搜索替换。
修正: 组件只使用 semantic 或 component token;primitive 只作为上层引用目标。
陷阱 2:Storybook 本地能打开就算完成
原因: 没有 CI 的组件目录可以在依赖升级后静默失效,它只是文档,不是安全网。
修正: 将 vitest --project=storybook、Storybook build 与高价值视觉 snapshot 纳入 Pull Request 检查。
陷阱 3:视觉 snapshot 一次覆盖太多
原因: 动画、日期、外部字体和随机 ID 会制造噪音,让评审者习惯直接接受更新。
修正: 固定动态内容,从主要组件状态开始;每次 snapshot 变化都由人查看差异。
陷阱 4:axe 通过就代表无障碍完成
原因: 自动工具无法完整判断文案语义、键盘流程质量和读屏器是否容易理解。
修正: 保留键盘与读屏器人工检查,并记录测试步骤和结果。
陷阱 5:让 Claude Code 一次迁移整个系统
原因: 文件范围、视觉差异和 API 变化同时扩大,失败后无法快速定位。
修正: 按组件拆分任务,先定义证明命令,验收当前批次后再扩大范围。
合并前检查清单
- 令牌名称表达用途,而不只表达外观
- 组件 props 足够少且保持稳定
- Storybook 包含 disabled、loading、error、focus、hover 状态
- 仅用键盘也能完成操作
- 必要处使用 ARIA,原生 HTML 已满足时不重复添加
- 人工查看过视觉 snapshot 差异
- Figma 差异被保存为可评审产物
- Claude Code 只编辑了指定目录
- 提示词、日志、story 与截图中没有密钥或客户私密数据
把清单写入项目指令,后续 Claude Code 会话就能复用同一验收标准。
下一步只做一件事
先在目标项目中建立 tokens.json,运行生成脚本,并挑现有 Button 补齐 Storybook 状态。确认 CSS variables 与 TypeScript 常量能够重复生成,npm run test:storybook -- --run、Storybook build 和视觉测试都能在 CI 重现,再迁移下一个组件。Figma 同步先保持只读报告模式。
如果团队需要一起确定设计系统的迁移范围、Storybook 引入方式、无障碍检查或 Claude Code 的权限边界,可查看培训与咨询页面。
实际测试结果
2026 年 7 月 22 日,我们从本文提取 build-tokens.mjs,并针对完整的 tokens.json 示例执行。正常用例以退出码 0 完成,生成了 25 个 CSS custom properties 和 TypeScript token map,且把 {primitive.color.blue.600} 正确解析为 #2563eb。加入未知引用的反向用例以非零退出码失败,并输出 Unknown token reference。JSON 片段、代码围栏和内部链接也经过检查,Storybook 命令已与当前官方 Vitest addon 和迁移文档核对。本网站仓库本身没有安装 Storybook,因此采用这套流程前,仍需在目标应用中实际执行组件与视觉测试。
相关文章
用 Claude Code 实践 Design Tokens:从 Figma 交接到 CSS 变量与 Tailwind
用 Claude Code 落地 Design Tokens,串起 Style Dictionary、CSS 变量、Tailwind 和 React。
用 Claude Code 设计 CSS 变量和主题 token
用 Claude Code 设计 CSS 自定义属性、var() fallback、主题 token、深色模式和检查清单。
用 Claude Code 做实用 API 设计:OpenAPI、测试与破坏性变更检查
用 Claude Code 设计可靠 REST API:OpenAPI 流程、Mock、测试、版本、安全与常见坑。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
让 Claude Code 真正进入可验证的工作流
先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。