Advanced (Mis à jour: 01/06/2026)

Construire un design system avec Claude Code : Design Tokens, Storybook et CI

Guide pratique pour relier Design Tokens, React/TypeScript, Storybook, accessibilité, tests visuels et CI.

Construire un design system avec Claude Code : Design Tokens, Storybook et CI

Un design system est une pratique d’exploitation, pas seulement une bibliothèque UI

Un design system ne se limite pas à des boutons et des cartes. Sa valeur vient surtout de la manière dont l’équipe change les couleurs, les espacements, les états, la documentation, les revues et les tests sans casser les écrans produit.

Claude Code est utile pour ce travail, car il peut lire le dépôt, modifier plusieurs fichiers, lancer Storybook et les tests, puis résumer les différences. Il ne remplace pas les décisions de marque, l’intention de Figma ni la validation humaine de l’accessibilité.

Ce guide couvre les Design Tokens, les composants React/TypeScript, Storybook, l’accessibilité, les tests visual/a11y en CI, les limites réalistes avec Figma et la bonne granularité de tâche pour Claude Code. À lire aussi : gestion des Design Tokens, développement Storybook et accessibilité avec Claude Code.

Architecture cible

Le contrat côté code esttokens.json. Figma reste un outil central de conception, mais le code doit disposer d’une source vérifiable par 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

Les Design Tokens sont des décisions de design nommées : couleurs, espaces, rayons, typographie et états. Un composant devrait utiliser un token sémantique commeaction.background.primary, plutôt qu’une valeur brute comme#2563eb.

Pour les références actuelles, consultez Design Tokens Community Group, Claude Code docs, Claude Code security, Storybook accessibility testing, Storybook visual tests, Playwright accessibility testing et Figma REST API.

La bonne granularité pour Claude Code

Évitez une demande vague comme “crée un design system”. Préférez : “migre uniquementButton, conserve l’API publique, ajoute les états Storybook et lance les tests a11y et visual”.

SujetBon périmètre pour Claude CodeDécision humaine
TokensExtraire couleurs et espacements répétésSens de marque et noms
ComposantsImplémenterButton, Input, Alert typésAPI publique et sémantique produit
StorybookAjouter variants, états et stories d’interactionÉtats nécessaires dans les parcours réels
AccessibilitéDétecter labels, focus et violations axeValidation UX et lecteur d’écran
CIConnecter visual/a11y checksPolitique de blocage et exceptions

Ajoutez des règles avant de lancer les modifications :

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.

La sécurité fait partie du flux. Ne collez pas de tokens Figma, npm tokens, secrets CI ou captures client dans les prompts. Relisez les commandes avant approbation et traitez les mises à jour massives de snapshots comme des décisions humaines.

Installation minimale

Exemple pour un projet React et 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

Scripts utiles :

{
  "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"
  }
}

Faire des tokens un contrat

Séparez primitive, semantic et 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}" }
      }
    }
  }
}

Générez CSS et 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.`);

Composant React/TypeScript

Voici un bouton typé avec variant, size, loading, disabled et focus visible.

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 et CI

Chaque état critique doit avoir une 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: "Enregistrer", variant: "primary" } };
export const Danger: Story = { args: { children: "Supprimer", variant: "danger" } };
export const Loading: Story = { args: { children: "Enregistrement", 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 a11y et 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, cas d’usage et pièges

Avec Figma, commencez par un rapport de différence plutôt qu’une synchronisation automatique.

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.

Trois cas reviennent souvent : une interface SaaS avec beaucoup d’états de formulaire, un produit white-label avec plusieurs marques, et un ancien CSS rempli de valeurs répétées. Dans les trois cas, Claude Code doit d’abord produire un inventaire et travailler par petits lots.

Les pièges sont concrets : trop de tokens primitifs, Storybook non exécuté en CI, snapshots visuels bruyants, confiance excessive dans axe, et migration trop large. Avant de généraliser, vérifiez quetokens.json génère CSS et TypeScript, que Storybook montre tous les états, et que la CI exécute build, a11y et visual tests de manière stable.

Pour une aide sur l’implémentation, la formation d’équipe, Storybook ou l’audit d’accessibilité avec Claude Code, consultez la page training et consultation.

#Claude Code #design system #Design Tokens #Storybook #accessibilité
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.