Advanced (Diperbarui: 1/6/2026)

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.

Membangun Design System dengan Claude Code: Design Tokens, Storybook, 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”.

AreaCocok untuk Claude CodeKeputusan manusia
TokensMengekstrak warna dan spacing berulangMakna brand dan nama token
ComponentsMembuatButton, Input, Alert bertipePublic API dan semantik produk
StorybookMenambah variant, state, interaction storyState yang penting di workflow nyata
AccessibilityMendeteksi label, focus, dan pelanggaran axeReview UX dan screen reader final
CIMenghubungkan visual/a11y checksKebijakan 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.

#Claude Code #design system #Design Tokens #Storybook #aksesibilitas
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.