Ein Design-System mit Claude Code aufbauen: Design Tokens, Storybook und CI
Praxisleitfaden für Design Tokens, React/TypeScript, Storybook, Barrierefreiheit, visuelle Tests und CI.
Ein Design-System ist ein Betriebsmodell, keine reine Komponenten-Galerie
Buttons, Cards und Inputs sind sichtbar, aber nicht der wichtigste Teil. Ein belastbares Design-System beschreibt, wie Farben, Abstände, Typografie, Zustände, Reviews und Tests dauerhaft geändert werden, ohne Produktseiten zu beschädigen.
Claude Code hilft dabei, weil es den bestehenden Code lesen, mehrere Dateien ändern, Storybook und Tests ausführen und die Änderungen zusammenfassen kann. Es ersetzt aber keine Markenentscheidung, keine Figma-Interpretation und keine finale Barrierefreiheitsprüfung.
Dieser Leitfaden verbindet Design Tokens, React/TypeScript-Komponenten, Storybook, Accessibility, visual/a11y Checks in CI, Figma-Grenzen und sinnvolle Aufgabenpakete für Claude Code. Ergänzend passen Design Tokens mit Claude Code, Storybook-Entwicklung und Barrierefreiheit mit Claude Code.
Zielarchitektur
Im Code isttokens.json der prüfbare Vertrag. Figma bleibt wichtig für Designarbeit, aber produktive Änderungen brauchen Review und 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 Tokens sind benannte Designentscheidungen: Farben, Abstände, Radien, Typografie und Zustände. Komponenten sollten semantische Tokens wieaction.background.primary verwenden, nicht rohe Werte wie#2563eb.
Aktuelle Referenzen: Design Tokens Community Group, Claude Code Docs, Claude Code Security, Storybook accessibility testing, Storybook visual tests, Playwright accessibility testing und Figma REST API.
Die richtige Aufgaben-Größe für Claude Code
“Baue ein Design-System” ist zu groß. Besser ist: “Migriere nurButton, erhalte die öffentliche API, ergänze Storybook-Zustände und führe a11y sowie visual tests aus.”
| Bereich | Geeignet für Claude Code | Menschliche Entscheidung |
|---|---|---|
| Tokens | Wiederholte Farben und Abstände finden | Markenbedeutung und Namen |
| Komponenten | TypisierteButton, Input, Alert erstellen | Öffentliche API und Produktsprache |
| Storybook | Varianten und Zustände ergänzen | Relevante reale Zustände |
| Accessibility | Labels, Fokus und axe-Verstöße finden | Finale UX- und Screenreader-Prüfung |
| CI | Visual/a11y Checks verdrahten | Blockierregeln und Ausnahmen |
Projektregeln helfen:
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.
Sicherheit gehört dazu. Figma Tokens, npm Tokens, CI Secrets und Kundenscreenshots gehören nicht in Prompts. Kommandos sollten vor der Freigabe geprüft werden, und große Snapshot-Updates brauchen menschliche Zustimmung.
Minimales Setup
Beispiel für React und TypeScript:
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
Nützliche Scripts:
{
"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"
}
}
Tokens als Vertrag
Trenne primitive, semantic und component Tokens.
{
"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}" }
}
}
}
}
Generiere CSS und 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-Komponente
Der Button enthält variant, size, loading, disabled und sichtbaren Fokus.
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 und CI
Jeder wichtige Zustand braucht eine 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: "Speichern", variant: "primary" } };
export const Danger: Story = { args: { children: "Löschen", variant: "danger" } };
export const Loading: Story = { args: { children: "Speichert", 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>
)
};
Tests:
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-Grenzen, Beispiele und Fehler
Starte bei Figma mit einem Differenzbericht, nicht mit blinder Synchronisierung.
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.
Drei typische Fälle sind SaaS-Admin-UIs mit vielen Formularzuständen, White-Label-Produkte mit Markenvarianten und Legacy-CSS mit wiederholten Rohwerten. Claude Code sollte zuerst inventarisieren und dann kleine PRs erzeugen.
Häufige Fehler sind primitive Tokens direkt in Komponenten, Storybook ohne CI, zu viele instabile Screenshots, blindes Vertrauen in axe und zu große Migrationen. Prüfe vor dem Rollout, dasstokens.json CSS und TypeScript erzeugt, Storybook alle Zustände rendert und CI build, a11y und visual tests stabil ausführt.
Für Implementierungshilfe, Team-Schulung, Storybook-Einführung oder Accessibility-Review mit Claude Code steht die Training- und Beratungsseite bereit.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude-Code-Permission-Receipt: Scope, Beweis und Rollback festhalten
Permission-Receipt für Claude Code: erlaubte Aktionen, Freigabegrenzen, Prüfbefehle, Rollback und Umsatz-CTA-Prüfung.
Sicheres Agent Harness fur Claude Code und Codex: Rechte, Prufung und Rollback
Ein praktisches Agent Harness fur Claude Code und Codex mit Policy, Plan, Verifikation und Recovery.
Claude Code Subagents: Praxisleitfaden für sichere Agent-Delegation
Claude Code Subagents praktisch nutzen: Artikel- und Codearbeit sicher aufteilen, Prompts einsetzen, Fehler vermeiden.