Membangun Design System dengan Claude Code: Design Tokens, Storybook, dan CI
Panduan memakai Claude Code untuk Design Tokens, React/TypeScript, Storybook, aksesibilitas, visual test, dan CI.
Design system adalah sistem kerja, bukan hanya katalog komponen
Banyak tim memulai design system dengan membuat button, card, dan input. Itu penting, tetapi nilai utamanya ada pada cara tim mengubah warna, spacing, typography, state, review, dan test tanpa merusak layar produk.
Claude Code cocok untuk pekerjaan ini karena bisa membaca codebase, mengedit beberapa file, menjalankan Storybook dan test, lalu merangkum diff. Namun Claude Code tidak boleh menggantikan keputusan brand, maksud desain di Figma, atau review aksesibilitas final.
Artikel ini membahas Design Tokens, komponen React/TypeScript, Storybook, aksesibilitas, visual/a11y check di CI, batas realistis integrasi Figma, dan ukuran task yang tepat untuk Claude Code. Untuk topik terkait, lihat manajemen Design Tokens, pengembangan Storybook, dan aksesibilitas dengan Claude Code.
Arsitektur Target
Di sisi code, jadikantokens.jsonsebagai kontrak yang bisa direview. Figma tetap penting untuk desain, tetapi perubahan yang masuk ke produk harus bisa diuji di 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 adalah keputusan desain yang disimpan sebagai data bernama: warna, jarak, radius, typography, dan state. Komponen sebaiknya memakai token semantik sepertiaction.background.primary, bukan nilai mentah seperti#2563eb.
Referensi yang berguna: Design Tokens Community Group, Claude Code docs, Claude Code security, Storybook accessibility testing, Storybook visual tests, Playwright accessibility testing, dan Figma REST API.
Ukuran Task yang Tepat untuk Claude Code
Prompt “buatkan design system” terlalu luas. Lebih aman meminta: “migrasikan hanyaButton, pertahankan public API, tambahkan state di Storybook, lalu jalankan a11y dan visual test”.
| Area | Cocok untuk Claude Code | Keputusan manusia |
|---|---|---|
| Tokens | Mengekstrak warna dan spacing berulang | Makna brand dan nama token |
| Components | MembuatButton, Input, Alert bertipe | Public API dan semantik produk |
| Storybook | Menambah variant, state, interaction story | State yang penting di workflow nyata |
| Accessibility | Mendeteksi label, focus, dan pelanggaran axe | Review UX dan screen reader final |
| CI | Menghubungkan visual/a11y checks | Kebijakan blocking dan exception |
Tulis aturan seperti ini sebelum Claude Code mengedit:
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.
Keamanan juga bagian dari workflow. Jangan tempel Figma token, npm token, CI secret, atau screenshot pelanggan ke prompt. Review command sebelum disetujui, dan perlakukan update snapshot besar sebagai perubahan yang perlu persetujuan manusia.
Setup Minimal
Contoh untuk proyek React dan 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
Tambahkan scripts yang bisa dipakai lokal dan CI:
{
"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"
}
}
Jadikan Design Tokens sebagai Kontrak
Pisahkan token menjadi primitive, semantic, dan 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}" }
}
}
}
}
Generate CSS variables dan TypeScript 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) {
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.`);
Komponen React/TypeScript
Button berikut memiliki variant, size, loading, disabled, dan focus ring yang terlihat.
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 dan CI
Setiap state penting harus ada di Storybook.
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: "Simpan", variant: "primary" } };
export const Danger: Story = { args: { children: "Hapus", variant: "danger" } };
export const Loading: Story = { args: { children: "Menyimpan", 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>
)
};
Jalankan a11y dan visual check:
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, Use Case, dan Kesalahan Umum
Untuk Figma, mulai dari laporan perbedaan, bukan sinkronisasi otomatis.
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.
Tiga use case yang sering muncul adalah dashboard SaaS dengan banyak state form, produk white-label dengan brand berbeda, dan cleanup CSS lama yang penuh nilai berulang. Dalam ketiganya, Claude Code sebaiknya mulai dari inventaris dan perubahan kecil.
Kesalahan umum: token primitive dipakai langsung di komponen, Storybook tidak masuk CI, visual snapshot terlalu berisik, axe dianggap menggantikan review manual, dan migrasi terlalu besar dalam satu task. Saat mencoba isi artikel ini, pastikantokens.jsonmenghasilkan CSS dan TypeScript, Storybook menampilkan semua state, dan CI menjalankan build, a11y, serta visual test dengan stabil.
Untuk bantuan implementasi design system, adopsi Storybook, review aksesibilitas, atau training workflow Claude Code untuk tim, gunakan halaman training dan konsultasi.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission receipt Claude Code: mencatat scope, bukti, dan rollback
Pola permission receipt untuk Claude Code: aksi yang diizinkan, batas approval, command verifikasi, rollback, dan cek CTA revenue.
Agent Harness Aman untuk Claude Code dan Codex: Permission, Verifikasi, dan Rollback
Rancang Agent Harness praktis untuk Claude Code dan Codex dengan policy, plan, verification, dan recovery layer.
Subagent Claude Code: panduan praktis untuk delegasi artikel dan kode
Panduan subagent Claude Code untuk membagi pekerjaan artikel dan kode: aturan delegasi, prompt, risiko, dan checklist.