用 Claude Code 构建设计系统:Design Tokens、Storybook 与 CI 实战
用 Claude Code 串联 Design Tokens、React/TypeScript、Storybook、无障碍与可视化测试,构建可维护的设计系统。
设计系统不是组件展示页,而是长期维护机制
很多团队一开始会先做按钮、卡片、输入框,但真正有价值的是一套能持续变化的机制:颜色怎么改、间距怎么统一、组件状态怎么评审、视觉回归和无障碍检查怎么进入 CI。
Claude Code 很适合这类工作。它能阅读现有代码、跨文件修改、运行 Storybook 和测试,并把差异整理出来。但它不能替代品牌判断、Figma 里的设计意图,也不能替代最终的无障碍人工评审。
本文会把 Design Tokens、React/TypeScript 组件、Storybook、无障碍检查、CI 中的 visual/a11y 测试,以及 Figma 联动的现实边界放在一个可执行流程中说明。相关主题也可以参考Design Tokens 管理、Storybook 开发和无障碍改进。
目标架构
建议把tokens.json作为代码侧的可审查契约。Figma 仍然是设计工作的重要入口,但进入生产代码前必须经过差异评审。
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 Tokens 是把颜色、字号、圆角、间距和状态等设计决定用名称和数据保存起来。组件应尽量使用action.background.primary这类语义名称,而不是直接写#2563eb。
参考资料建议查看 Design Tokens Community Group、Claude Code 文档、Claude Code security、Storybook accessibility testing、Storybook visual tests、Playwright accessibility testing和 Figma REST API。
Claude Code 适合处理的粒度
不要让 Claude Code 一次性“做完整设计系统”。更好的任务是:“只迁移Button,保留现有 API,补齐 Storybook 状态,运行 a11y 和 visual test”。
| 领域 | 适合交给 Claude Code | 需要人工决定 |
|---|---|---|
| Tokens | 从 CSS 中提取重复颜色和间距 | 品牌含义和命名原则 |
| Components | 实现类型安全的Button、Input、Alert | 对外 API 和产品语义 |
| Storybook | 增加 variants、states、interaction stories | 哪些状态真正重要 |
| Accessibility | 查找 label、focus、axe 违规 | 最终 UX 和读屏体验 |
| CI | 接入 visual/a11y 检查 | 阻断规则和例外流程 |
可以把下面这段规则放进项目说明,再让 Claude Code 开始改代码。
Design system task rules:
- Edit only src/components, src/styles, .storybook, tests, scripts, and tokens.json.
- Do not change brand colors without listing old and new token names.
- Every new component needs TypeScript props, keyboard behavior, Storybook stories, and a11y notes.
- Run npm run tokens:build, npm run test:storybook, npm run test:a11y, and npm run test:visual before reporting done.
- If focus behavior changes, include manual review steps.
权限和安全也要写清楚。不要把 Figma token、npm token、CI secret、客户截图直接交给 Claude Code。执行命令前要看清楚,涉及大量 snapshot 更新时应由人确认。
最小安装
下面以 React、TypeScript 和工具类 CSS 为前提。
npm install class-variance-authority clsx tailwind-merge
npm install -D @storybook/react-vite @storybook/addon-a11y @storybook/test-runner @playwright/test @axe-core/playwright concurrently http-server wait-on
npx storybook init
npx playwright install chromium
在package.json中准备可复现的命令。
{
"scripts": {
"tokens:build": "node scripts/build-tokens.mjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test:storybook": "test-storybook --url http://127.0.0.1:6006",
"test:a11y": "playwright test tests/a11y.spec.ts",
"test:visual": "playwright test tests/button.visual.spec.ts"
}
}
先把 Design Tokens 做成契约
推荐分三层:primitive 保存原始值,semantic 表达语义,component 表达具体组件状态。
{
"primitive": {
"color": {
"blue": {
"50": { "$type": "color", "$value": "#eff6ff" },
"600": { "$type": "color", "$value": "#2563eb" },
"700": { "$type": "color", "$value": "#1d4ed8" }
},
"gray": {
"50": { "$type": "color", "$value": "#f9fafb" },
"200": { "$type": "color", "$value": "#e5e7eb" },
"900": { "$type": "color", "$value": "#111827" }
},
"red": {
"600": { "$type": "color", "$value": "#dc2626" },
"700": { "$type": "color", "$value": "#b91c1c" }
},
"white": { "$type": "color", "$value": "#ffffff" }
},
"space": {
"2": { "$type": "dimension", "$value": "0.5rem" },
"3": { "$type": "dimension", "$value": "0.75rem" },
"4": { "$type": "dimension", "$value": "1rem" },
"6": { "$type": "dimension", "$value": "1.5rem" }
},
"radius": {
"md": { "$type": "dimension", "$value": "0.375rem" },
"lg": { "$type": "dimension", "$value": "0.5rem" }
}
},
"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 常量:
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) {
if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
return resolveValue(getToken(value.slice(1, -1)));
}
return value;
}
function walk(node, pathParts = [], result = {}) {
if (node && typeof node === "object" && typeof node.$value !== "undefined") {
result[pathParts.join("-")] = resolveValue(node.$value);
return result;
}
for (const [key, value] of Object.entries(node)) {
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.`);
React/TypeScript 组件
组件 API 要稳定、少而清晰。下面的Button包含 variant、size、loading、disabled 和 focus ring。
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>
);
});
Storybook 作为规格书
Storybook 不是截图仓库,而是组件状态的规格书。所有关键状态都应该有 story。
import type { Meta, StoryObj } from "@storybook/react";
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: "保存", variant: "primary" } };
export const Danger: Story = { args: { children: "删除", variant: "danger" } };
export const Loading: Story = { args: { children: "保存中", 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>
)
};
CI 中的 a11y 与 visual 测试
自动化不能替代人工检查,但可以阻止明显问题进入主分支。
import { expect, test } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
const storyPaths = [
"/iframe.html?id=design-system-button--primary",
"/iframe.html?id=design-system-button--danger",
"/iframe.html?id=design-system-button--loading",
"/iframe.html?id=design-system-button--all-states"
];
for (const storyPath of storyPaths) {
test(`a11y ${storyPath}`, async ({ page }) => {
await page.goto(`http://127.0.0.1:6006${storyPath}`);
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(results.violations).toEqual([]);
});
}
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"
});
});
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: npm run build-storybook
- run: npx playwright install --with-deps chromium
- 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:storybook && npm run test:a11y && npm run test:visual"
Figma 联动的现实边界
Figma Variables 很重要,但不要一开始就做无审查的双向同步。更安全的做法是先生成差异报告。
Read figma-tokens-export.json and tokens.json.
Create a markdown report with:
1. tokens that exist in Figma but not in code
2. tokens that exist in code but not in Figma
3. value differences for matching semantic tokens
Do not edit tokens.json. Do not rename tokens. Mark risky differences around focus, danger, and text color.
Figma 负责表达设计意图,代码负责可测试的契约。两者之间需要评审,而不是盲目同步。
实例、失败点与确认
常见用例有三个。第一是 SaaS 管理后台,按钮、表单、表格、弹窗状态很多,适合先迁移Button和Input。第二是白标产品,不同客户有不同品牌色,但语义 token 保持稳定。第三是旧 CSS 清理,Claude Code 可以把重复颜色和间距整理成迁移表。
失败点也很具体:只增加 primitive token 会让组件继续依赖颜色名;Storybook 不进入 CI 就无法防回归;visual test 覆盖太多会因为动画、日期、字体产生噪声;axe 通过不代表读屏体验一定好;一次性迁移所有组件会让 review 失控。
实际试用时,请先确认tokens.json能生成 CSS 和 TypeScript,Button的全部状态能在 Storybook 中显示,CI 能稳定运行 Storybook build、a11y 和 visual test。Figma 联动先保持报告模式,等团队确认代码与设计的正本关系后再扩大自动化。
如果团队需要设计系统落地、Storybook 导入、无障碍审查或 Claude Code 工作流培训,可以从培训与咨询页面联系我们。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。