Yes, you can describe a theme in English and get a working design system. The catch is that the English description is not a prompt. It is a source file. And like any source file, it needs a compiler, a type system, and tests.

If you treat it like a chatbot query, you will get a different color palette on Tuesday than you got on Monday. If you treat it like a DSL with a bounded context and a strict schema, you get reproducible tokens that your design system can consume.

Design tokens are a synchronization problem, not a creative one

Most teams do not struggle to pick colors. They struggle to keep the colors consistent between Figma, CSS variables, and component libraries. A typical workflow looks like this: a designer updates a hex code in a style guide, a developer copies it into a JSON file, another developer references the wrong key in a React component, and three months later you have #1a1a2e in one place and #1b1b2f in another.

The real pain is the handoff. Figma is not code. JSON is not a design tool. English sits in the middle. It is the only format both designers and developers can read without training.

The question is not whether an LLM can turn English into hex codes. The question is whether you can turn that process into something reliable enough to run in CI.

How theme-to-code actually works

The architecture is straightforward. You write a short theme manifest in plain English. A script feeds it to an LLM with a constrained output format. The LLM returns structured tokens. You write those tokens to disk, type-check them, and commit them.

Here is the pipeline:

  1. The manifest: A .txt or .md file that describes the theme in prose.
  2. The schema: A Zod (or JSON Schema) definition that the LLM must return.
  3. The compiler: A script that reads the manifest, calls the LLM with structured outputs, and writes a tokens.json.
  4. The gate: A test that fails if the generated tokens deviate from a snapshot or violate constraints.

The key is step three. You are not asking the model to “generate a dark theme.” You are asking it to populate a schema. That turns a creative writing task into a structured data extraction task, which LLMs are significantly better at.

A working compiler in TypeScript

Here is a minimal but complete implementation. It reads a theme description, calls OpenAI with a Zod schema, and writes the result to a JSON file.

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();

You will need zod, openai, and zod-to-json-schema as dependencies. The script assumes you are using Bun, but swapping Bun.file and Bun.write for fs.promises is trivial.

The trade-offs you cannot ignore

This works, but it is not free.

Determinism. Even with temperature set to zero and structured outputs enabled, the same description can produce slightly different hex codes across model versions. If you rerun the compiler after an OpenAI update, your snapshot tests may fail. You should commit the generated tokens.json and only recompile when the manifest changes.

Latency. Calling an LLM in your build step adds seconds, sometimes tens of seconds. Do not compile themes on every hot reload. Run the compiler as a pre-commit hook or in CI when the manifest file changes.

Schema rigidity. The LLM cannot invent new token categories. If your design system later needs elevation.shadows.xl, you must update the Zod schema, the system prompt, and the compiler before the model can emit it. That is a feature, not a bug. It keeps the DSL bounded.

Accessibility. An LLM does not know if your primary color against your background meets WCAG contrast ratios. You must validate the generated tokens with a separate check. Add this after the TokenSchema.parse call:

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`);
}

When this breaks down: the interaction problem

This approach works beautifully for color palettes, spacing scales, and typography families. It falls apart when tokens interact in ways that are hard to describe in prose.

Semantic tokens are the classic trap. You might write “use the primary color for links and buttons.” But what about disabled buttons? What about links inside a warning banner that already uses the primary color? These are not theme-level decisions. They are component-level rules, and they belong in your component library, not in a manifest file.

Keep the manifest bounded to primitive tokens. Let your components handle semantics.

Frequently asked questions

What is a bounded-context DSL? A small language with a narrow scope. In this case, the “language” is English prose that only describes design tokens, governed by a strict schema.

Will the same description always produce the same tokens? Not always. LLM outputs vary across model versions and providers. Commit the generated tokens and use snapshot tests to detect drift.

Can I use this for component-level styles? No. The manifest should only define primitive tokens like colors and spacing. Component semantics belong in your component library.

Do I need OpenAI, or will other models work? Any model that supports structured JSON output and follows a system prompt will work. Local models like Llama 3.3 are viable if you have the hardware to run them fast enough for your build pipeline.

Start with a ten-line manifest and a strict schema

Pick one bounded context, like a marketing landing page or an admin dashboard. Write a ten-line manifest, define a Zod schema with no optional fields, and generate your first token file. Commit the output. Write a test that recompiles the manifest and compares it to the committed snapshot.

If the snapshot drifts without the manifest changing, your compiler is not deterministic. Fix the system prompt or pin the model version until it is.

Your design system does not need another Figma plugin. It needs a source of truth that both designers and developers can read. Plain English is readable. The schema makes it reliable.