Theme Generator: Compiling JSON Design Tokens into @theme Blocks Automatically
AI generated
</>
tw
Tailwind CSS · Design Tokens · Theme Generator · Build Pipeline
Theme Generator
Compiling JSON Design Tokens into @theme Blocks Automatically

A manually maintained @theme block spirals out of control once the same color values are also needed for native apps, Figma, or a second frontend. A theme generator reads a single JSON token file as the single source of truth and automatically produces the Tailwind CSS v4 @theme block from it, consistent across every platform.

18 min read JSON tokens · Node generator · @theme · CI validation Tailwind CSS v4 · Cross-Platform Theming

1. Why a manually maintained @theme block spirals out of control

A single, hand written @theme block in Tailwind CSS v4 is entirely sufficient for a single frontend project. But once the same color values, spacing, and font sizes are also needed for a native mobile app, a design file in Figma, or a second, separate frontend, a classic synchronization problem arises. A theme generator solves exactly this problem by maintaining the values in a single place only.

Without a theme generator, teams typically maintain the same color values twice or three times: once in the Tailwind @theme block, once in a native constants file for the mobile app, once as a Figma style. If a single brand color changes, all three places must be kept in sync manually, which in practice almost inevitably leads to discrepancies that only surface late, often only during a visual comparison between web and app.

The structural way out is to stop maintaining design tokens in a CSS specific format and instead maintain them in a platform neutral JSON format, from which a theme generator automatically produces every required output format. The Tailwind @theme block thereby becomes one of several generated artifacts, no longer the single source of truth.

2. A JSON token file as the single source of truth

The first step toward a working theme generator is defining a consistent JSON format for all design tokens. A proven structure follows the community proposal from the W3C Design Tokens Community Group, where every token carries a $value and optionally a $type, nested in a thematic tree structure like color.brand.primary or spacing.card.padding.

This JSON file becomes the only place where a developer or designer actually changes a color value. Every other format, the Tailwind @theme block, native mobile constants, or a Figma sync file, is produced exclusively by running the theme generator, never through manual editing. That structurally prevents generated files and the source from drifting apart, because there simply is no second place where a value could be changed manually.

For teams with several brands or themes, the same JSON structure can be extended with an additional level, for example themes.default.color.brand.primary and themes.dark.color.brand.primary, so a theme generator can produce both the default and the dark mode @theme override from the same source.


{
  "color": {
    "brand": {
      "primary": { "$type": "color", "$value": "#0ea5e9" },
      "secondary": { "$type": "color", "$value": "#0c4a6e" }
    },
    "surface": {
      "default": { "$type": "color", "$value": "#ffffff" },
      "dark": { "$type": "color", "$value": "#0f172a" }
    }
  },
  "spacing": {
    "card": {
      "padding": { "$type": "dimension", "$value": "1.5rem" }
    }
  },
  "font": {
    "heading": { "$type": "fontFamily", "$value": "Inter, sans-serif" }
  }
}

3. The generator's basic building block

The actual theme generator is a small Node.js script that reads the JSON token file, recurses through the nested structure, and produces a CSS custom property inside an @theme block for every $value entry found. The recursion assembles the full path into the variable name along the way, so color.brand.primary becomes --color-brand-primary.

The generator should be deliberately kept simple, a pure transformation script without heavy dependencies, so it stays easy to maintain and offers little surface for build failures. For most projects, a few dozen lines of JavaScript are enough, without a heavy design token framework as a dependency, though for very complex multi platform setups, established tools like Style Dictionary, which already implement the same basic idea, are also worth considering.

An important detail: the theme generator should mark the generated file with a clear comment header warning against manual edits and pointing back to the JSON source. That prevents a developer from accidentally changing a value directly in the generated CSS file, which would just be overwritten again on the next generator run.


// theme-generator.mjs — transforms JSON design tokens into a Tailwind @theme block
import { readFileSync, writeFileSync } from 'node:fs';

const tokens = JSON.parse(readFileSync('./tokens/tokens.json', 'utf-8'));

/**
 * Recursively flattens the nested token tree into CSS custom property lines.
 */
function flatten(node, path = []) {
  const lines = [];

  for (const [key, value] of Object.entries(node)) {
    const currentPath = [...path, key];

    if (value && typeof value === 'object' && '$value' in value) {
      const varName = `--${currentPath.join('-')}`;
      lines.push(`  ${varName}: ${value.$value};`);
    } else if (value && typeof value === 'object') {
      lines.push(...flatten(value, currentPath));
    }
  }

  return lines;
}

const cssLines = flatten(tokens);

const output = [
  '/* AUTO-GENERATED FILE — do not edit directly.',
  '   Source of truth: tokens/tokens.json */',
  '@theme {',
  ...cssLines,
  '}',
  '',
].join('\n');

writeFileSync('./src/css/theme-generated.css', output);
console.log(`Generated ${cssLines.length} design tokens.`);

4. Naming convention: from JSON path to CSS variable

A consistent naming convention between JSON path and CSS variable is the foundation of every maintainable theme generator. The simplest and most traceable rule is a direct one to one translation: every nesting level in the JSON gets appended to the variable name separated by a hyphen, so color.brand.primary becomes --color-brand-primary, with no special cases or renaming.

This direct translation has a practical advantage: a developer who sees a CSS variable in the code can immediately find the corresponding path in the JSON source, without needing to look up a separate mapping table. A theme generator that instead renames or abbreviates variable names, for example --c-b-p instead of --color-brand-primary, saves a few characters but considerably harms traceability, and should be avoided.

For Tailwind specific prefixes like --color-* for colors or --spacing-* for spacing, it is worth validating inside the generator itself that the top level JSON key is actually one of the namespaces Tailwind recognizes. That way the theme generator prevents a typo in the JSON structure from producing a custom property that Tailwind generates but that no utility class ever consumes.

5. Multiple output formats from one source

The actual value of a theme generator shows once several different output formats are produced from the same JSON source. Alongside the Tailwind @theme block, the same token tree can easily be translated into a Colors.swift file for iOS, a colors.xml resource for Android, or a JSON file in the Figma tokens plugin format, each with its own small transformation function taking the same JSON source as input.

This multiple output works most cleanly when the theme generator is internally split into two clearly separated steps: first reading and validating the JSON source into a normalized, flat intermediate representation, then a series of independent output functions that each only consume this intermediate representation. Adding a new output format, for example for a future React Native project, then just means adding one more output function, without touching the rest of the generator.

For design teams, an automated sync with Figma via its REST API is also worthwhile, so designers always work with the same tokens that also end up in the production Tailwind build. The theme generator thereby becomes the central interface between design and development, rather than a purely internal developer build helper.

6. Validating the token file before generation

A theme generator without validation carries erroneous values into every generated output without comment. A typo like #0ea5e instead of #0ea5e9, a missing percent sign on an HSL value, or an empty $value should stop the build process with a clear error message, instead of shipping broken CSS that only fails in the browser.

A simple but effective approach is a validation function that runs over the token tree before the actual generation and checks the format of every detected $value against its $type. For $type: "color", for example, a regular expression for valid hex codes or RGB function notation, for $type: "dimension" a check for a valid CSS unit. The theme generator aborts on a validation error with a clear pointer to the affected token path, instead of silently passing an invalid value through.

For larger teams, a formal JSON schema checked in the CI pipeline before the actual generator run is also recommended. That pushes errors even further forward in the process, ideally directly into the pull request check, before any build even starts.

7. Integration into the build pipeline

The theme generator must reliably run before the actual Tailwind build, so the generated @theme file already exists when Tailwind starts its compilation. In practice that means an npm run tokens script that runs automatically as a pre hook before the regular npm run build script, so developers never have to keep the order in mind manually.

For local development, a watch mode that automatically reruns the theme generator on every change to the JSON token file, alongside the already running Tailwind watch process, is also worthwhile. That way a developer sees a color change in the JSON reflected practically in real time in the browser, without manually restarting the build process.

For continuous integration environments, the generator run should be part of the same build step as the Tailwind compilation, so a faulty token never ends up unnoticed in a deployment. A failed theme generator run should hard fail the entire build, not just emit a warning that could be overlooked in the CI logs.


# package.json scripts excerpt
# "pretokens": runs automatically before "tokens"
# "prebuild": ensures tokens are generated before Tailwind compiles

npm run tokens        # runs theme-generator.mjs once
npm run tokens:watch   # re-runs on every change to tokens/tokens.json
npm run build          # prebuild triggers "tokens", then compiles Tailwind

# CI pipeline step
npm run tokens && npm run build

8. Versioning and diffing token changes

Because the generated @theme file arises from a single JSON source, a Git diff on that JSON file becomes the central place where reviewers can follow a theme generator change. A pull request that changes a brand color shows exactly one changed line in the JSON file, instead of scattered changes across several generated output files, which considerably eases code review.

The generated output files themselves, for example the compiled @theme CSS file, should ideally not even be checked into version control, but treated as a build artifact produced fresh on every build. That keeps the repository clean and prevents merge conflicts in generated files that are derived from the JSON source anyway.

For projects where the generated file must be checked in for practical reasons, for example because no build step exists before deployment, a CI check that reruns the theme generator and compares the result against the checked in version is worthwhile. If the result deviates, the build fails, which reliably prevents anyone from accidentally editing the generated file manually without adjusting the JSON source accordingly.


# CI check: regenerate and fail if the committed file drifted from source
npm run tokens
git diff --exit-code src/css/theme-generated.css || {
  echo "theme-generated.css is out of sync with tokens/tokens.json";
  exit 1;
}

9. Manual block versus generated block compared

The following comparison shows when the extra effort of a theme generator actually pays off against a hand maintained @theme block.

Criterion Manual @theme block Generated block from JSON Assessment
Single source of truth Only valid for the web Applies across platforms Generator clearly ahead for multi-platform
Consistency with native apps Manual sync required Automatic from the same source Less risk of drift
Setup effort None, directly writable Generator script required Manual block faster for small projects
Value validation None, typos go unnoticed Centralized in the generator Generator catches errors early
Review readability Directly visible in the CSS One layer of indirection via JSON Both workable, different styles

For a single web project without native apps or design tool synchronization, a manually maintained @theme block often remains the simpler choice. But once more than one platform needs the same design tokens, the effort of a theme generator quickly outweighs the cost of duplicated manual maintenance.

Mironsoft

Tailwind CSS v4, design token pipelines, and build automation

One token source for web, app, and Figma?

We build theme generator pipelines that automatically translate JSON design tokens into Tailwind CSS v4, native mobile formats, and Figma styles, including validation and CI protection.

Token architecture

Defining JSON structure and naming convention for your design system

Generator development

Building a multi-output generator with validation and CI diff check production ready

Pipeline integration

Wiring watch mode, build hooks, and Figma sync into existing processes

10. Summary

A theme generator shifts design tokens from a CSS specific, manually maintained @theme block toward a platform neutral JSON source, from which a small transformation script automatically produces every required output format. The naming convention translates JSON paths directly and traceably into CSS variable names, while validation ensures faulty values stop the build process instead of silently reaching production.

The actual payoff shows once more than one platform needs the same tokens: a theme generator keeps web, native apps, and design tools automatically in sync, without a team having to manually reconcile three separate color definitions. For projects without a multi platform need, a simple, manual @theme block remains the more pragmatic choice.

Theme Generator for Design Tokens — The Essentials at a Glance

JSON as single source

A platform neutral token file replaces repeatedly maintained color definitions.

Direct naming convention

JSON path and CSS variable name are structured identically, no hidden mappings.

Validation before generation

Faulty values stop the build instead of silently reaching generated CSS.

CI diff check

Prevents generated files from being edited manually and drifting out of sync.

11. FAQ: Theme Generator for Design Tokens

1When does a theme generator pay off?
As soon as the same tokens are also needed for native apps, Figma, or a second frontend.
2Which JSON format works well?
The W3C design tokens proposal with $value and $type per token in a tree structure.
3How is the variable name formed?
Direct one to one translation, each level appended to the name separated by a hyphen.
4Which output formats are possible?
Tailwind @theme, native iOS/Android constants, and Figma tokens files from the same source.
5What happens with faulty values?
Validation stops the build with a clear error message instead of shipping broken CSS.
6Must the generated file be checked in?
Ideally not, a CI diff check protects it if it must be checked in anyway.
7How does pipeline integration work?
As an npm pre hook before the Tailwind build, complemented by watch mode for local development.
8Does the generator complicate reviews?
On the contrary, reviews only touch the changed JSON line, not several generated files.
9Do you need a ready made framework?
Not necessarily, a simple Node script is usually enough, Style Dictionary for more complex setups.
10How are multiple themes represented?
Through an additional level like themes.default and themes.dark in the token tree.