Oui, vous pouvez décrire un thème en anglais et obtenir un design system fonctionnel. L’astuce est que la description en anglais n’est pas un prompt. C’est un fichier source. Et comme tout fichier source, il a besoin d’un compilateur, d’un système de types et de tests.

Si vous le traitez comme une requête de chatbot, vous obtiendrez une palette de couleurs différente le mardi de celle du lundi. Si vous le traitez comme un DSL avec un contexte borné et un schema strict, vous obtenez des tokens reproductibles que votre design system peut consommer.

Les design tokens sont un problème de synchronisation, pas un problème créatif

La plupart des équipes n’ont pas de mal à choisir des couleurs. Elles ont du mal à garder les couleurs cohérentes entre Figma, les variables CSS et les component libraries. Un workflow typique ressemble à ceci : un designer met à jour un code hex dans un guide de style, un développeur le copie dans un fichier JSON, un autre développeur référence la mauvaise clé dans un composant React, et trois mois plus tard vous avez #1a1a2e à un endroit et #1b1b2f à un autre.

La vraie douleur est la transmission. Figma n’est pas du code. JSON n’est pas un outil de design. L’anglais est au milieu. C’est le seul format que designers et développeurs peuvent lire sans formation.

La question n’est pas de savoir si un LLM peut transformer l’anglais en codes hex. La question est de savoir si vous pouvez transformer ce processus en quelque chose de suffisamment fiable pour tourner en CI.

Comment theme-to-code fonctionne en réalité

L’architecture est simple. Vous écrivez un manifest de thème court en anglais simple. Un script l’alimente dans un LLM avec un format de sortie contraint. Le LLM retourne des tokens structurés. Vous écrivez ces tokens sur disque, les type-checks et les commitez.

Voici le pipeline :

  1. Le manifest : Un fichier .txt ou .md qui décrit le thème en prose.
  2. Le schema : Une définition Zod (ou JSON Schema) que le LLM doit retourner.
  3. Le compilateur : Un script qui lit le manifest, appelle le LLM avec des outputs structurés et écrit un tokens.json.
  4. La barrière : Un test qui échoue si les tokens générés dévient d’un snapshot ou violent des contraintes.

La clé est l’étape trois. Vous ne demandez pas au modèle de “générer un thème sombre.” Vous lui demandez de remplir un schema. Cela transforme une tâche d’écriture créative en une tâche d’extraction de données structurées, dans laquelle les LLMs sont significativement meilleurs.

Un compilateur fonctionnel en TypeScript

Voici une implémentation minimale mais complète. Elle lit une description de thème, appelle OpenAI avec un schema Zod et écrit le résultat dans un fichier JSON.

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import OpenAI from "openai";

// 1. Define the bounded context: only these tokens exist
const TokenSchema = z.object({
  name: z.string(),
  colors: z.object({
    background: z.string().regex(/^#[0-9a-f]{6}$/i),
    surface: z.string().regex(/^#[0-9a-f]{6}$/i),
    primary: z.string().regex(/^#[0-9a-f]{6}$/i),
    text: z.string().regex(/^#[0-9a-f]{6}$/i),
    muted: z.string().regex(/^#[0-9a-f]{6}$/i),
  }),
  spacing: z.object({
    unit: z.number().min(4).max(16),
    scale: z.array(z.number()).length(4),
  }),
  radii: z.object({
    sm: z.number(),
    md: z.number(),
    lg: z.number(),
  }),
});

type TokenSet = z.infer<typeof TokenSchema>;

// 2. The compiler
async function compileTheme(
  description: string,
  apiKey: string
): Promise<TokenSet> {
  const client = new OpenAI({ apiKey });

  const completion = await client.chat.completions.create({
    model: "gpt-4o",
    messages: [
      {
        role: "system",
        content:
          "You are a design-token compiler. " +
          "Convert the user's theme description into the exact JSON schema provided. " +
          "All colors must be valid 6-digit hex. " +
          "The spacing unit must be a multiple of 4. " +
          "The scale array must have exactly 4 values.",
      },
      {
        role: "user",
        content: description,
      },
    ],
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "theme_tokens",
        strict: true,
        schema: zodToJsonSchema(TokenSchema),
      },
    },
  });

  const raw = JSON.parse(completion.choices[0].message.content!);
  return TokenSchema.parse(raw);
}

// 3. CLI entrypoint
async function main() {
  const description = await Bun.file("theme.manifest.txt").text();
  const tokens = await compileTheme(description, process.env.OPENAI_API_KEY!);
  await Bun.write("tokens.json", JSON.stringify(tokens, null, 2));
  console.log("Compiled theme:", tokens.name);
}

main();

Vous aurez besoin de zod, openai et zod-to-json-schema comme dependencies. Le script suppose que vous utilisez Bun, mais remplacer Bun.file et Bun.write par fs.promises est trivial.

Les trade-offs que vous ne pouvez pas ignorer

Ça marche, mais ce n’est pas gratuit.

Déterminisme. Même avec la temperature à zéro et les outputs structurés activés, la même description peut produire des codes hex légèrement différents selon les versions de modèle. Si vous réexécutez le compilateur après une mise à jour d’OpenAI, vos snapshot tests peuvent échouer. Vous devriez commiter le tokens.json généré et ne recompiler que lorsque le manifest change.

Latence. Appeler un LLM dans votre étape de build ajoute des secondes, parfois des dizaines de secondes. Ne compilez pas les thèmes à chaque hot reload. Exécutez le compilateur comme un pre-commit hook ou en CI lorsque le fichier manifest change.

Rigidité du schema. Le LLM ne peut pas inventer de nouvelles catégories de tokens. Si votre design system a besoin de elevation.shadows.xl plus tard, vous devez mettre à jour le schema Zod, le system prompt et le compilateur avant que le modèle puisse l’émettre. C’est une feature, pas un bug. Ça maintient le DSL borné.

Accessibilité. Un LLM ne sait pas si votre couleur primary sur votre background respecte les ratios de contraste WCAG. Vous devez valider les tokens générés avec une vérification séparée. Ajoutez ceci après l’appel à TokenSchema.parse :

function contrastRatio(hex1: string, hex2: string): number {
  // WCAG relative luminance calculation
  const lum = (hex: string) => {
    const rgb = parseInt(hex.slice(1), 16);
    const [r, g, b] = [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];
    const f = (c: number) => {
      c /= 255;
      return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    };
    return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
  };
  const l1 = lum(hex1) + 0.05;
  const l2 = lum(hex2) + 0.05;
  return l1 > l2 ? l1 / l2 : l2 / l1;
}

// After parsing tokens
const ratio = contrastRatio(tokens.colors.primary, tokens.colors.background);
if (ratio < 4.5) {
  throw new Error(`Contrast ratio ${ratio.toFixed(2)} fails WCAG AA`);
}

Quand ça s’effondre : le problème d’interaction

Cette approche fonctionne magnifiquement pour les palettes de couleurs, les spacing scales et les familles typographiques. Elle s’effondre lorsque les tokens interagissent de manières difficiles à décrire en prose.

Les semantic tokens sont le piège classique. Vous pourriez écrire “utilisez la couleur primary pour les links et les buttons.” Mais qu’en est-il des buttons désactivés ? Qu’en est-il des links à l’intérieur d’une bannière d’avertissement qui utilise déjà la couleur primary ? Ce ne sont pas des décisions au niveau du thème. Ce sont des règles au niveau du composant, et elles appartiennent à votre component library, pas à un fichier manifest.

Gardez le manifest limité aux primitive tokens. Laissez vos components gérer la sémantique.

Questions fréquemment posées

Qu’est-ce qu’un DSL à contexte borné ? Un petit langage avec une portée étroite. Dans ce cas, le “langage” est de la prose en anglais qui décrit uniquement des design tokens, régie par un schema strict.

La même description produira-t-elle toujours les mêmes tokens ? Pas toujours. Les outputs de LLM varient selon les versions de modèle et les providers. Commitez les tokens générés et utilisez des snapshot tests pour détecter le drift.

Puis-je utiliser cela pour des styles au niveau du composant ? Non. Le manifest ne doit définir que des primitive tokens comme des couleurs et du spacing. La sémantique des composants appartient à votre component library.

Ai-je besoin d’OpenAI, ou d’autres modèles fonctionneront-ils ? Tout modèle supportant des outputs JSON structurés et suivant un system prompt fonctionnera. Des modèles locaux comme Llama 3.3 sont viables si vous avez le hardware pour les exécuter assez vite pour votre build pipeline.

Commencez avec un manifest de dix lignes et un schema strict

Choisissez un contexte borné, comme une landing page marketing ou un admin dashboard. Écrivez un manifest de dix lignes, définissez un schema Zod sans champs optionnels et générez votre premier fichier de tokens. Commitez l’output. Écrivez un test qui recompile le manifest et le compare au snapshot commite.

Si le snapshot drifte sans que le manifest n’ait changé, votre compilateur n’est pas déterministe. Corrigez le system prompt ou figez la version du modèle jusqu’à ce qu’il le soit.

Votre design system n’a pas besoin d’un autre plugin Figma. Il a besoin d’une source de vérité que designers et développeurs peuvent lire. L’anglais simple est lisible. Le schema le rend fiable.