Design-Tokens aus JSON automatisch in @theme-Blöcke kompilieren
Ein manuell gepflegter @theme-Block gerät außer Kontrolle, sobald dieselben Farbwerte auch für native Apps, Figma oder ein zweites Frontend gebraucht werden. Ein Theme-Generator liest eine einzige JSON-Token-Datei als Single Source of Truth und erzeugt daraus automatisch den Tailwind CSS v4 @theme-Block, konsistent über alle Plattformen hinweg.
Inhaltsverzeichnis
- 1. Warum ein manuell gepflegter @theme-Block außer Kontrolle gerät
- 2. Eine JSON-Token-Datei als Single Source of Truth
- 3. Der Generator-Grundbaustein
- 4. Namenskonvention: von JSON-Pfad zu CSS-Variable
- 5. Mehrere Ausgabeformate aus einer Quelle
- 6. Validierung der Token-Datei vor der Generierung
- 7. Integration in die Build-Pipeline
- 8. Versionierung und Diffing von Token-Änderungen
- 9. Manueller Block versus generierter Block im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein manuell gepflegter @theme-Block außer Kontrolle gerät
Ein einzelner, von Hand geschriebener @theme-Block in Tailwind CSS v4 ist für ein einzelnes Frontend-Projekt völlig ausreichend. Sobald aber dieselben Farbwerte, Abstände und Schriftgrößen auch für eine native Mobile-App, eine Design-Datei in Figma oder ein zweites, separates Frontend gebraucht werden, entsteht ein klassisches Synchronisationsproblem. Ein Theme-Generator löst genau dieses Problem, indem er die Werte nur an einer einzigen Stelle pflegt.
Ohne Theme-Generator pflegen Teams dieselben Farbwerte typischerweise doppelt oder dreifach: einmal im Tailwind-@theme-Block, einmal in einer nativen Konstanten-Datei für die Mobile-App, einmal als Figma-Style. Ändert sich ein einziger Markenfarbton, müssen alle drei Stellen manuell synchron gehalten werden, was in der Praxis fast zwangsläufig zu Abweichungen führt, die erst spät auffallen, oft erst im visuellen Vergleich zwischen Web und App.
Der strukturelle Ausweg ist, Design-Tokens nicht mehr in einem CSS-spezifischen Format zu pflegen, sondern in einem plattformneutralen JSON-Format, aus dem ein Theme-Generator automatisch alle benötigten Ausgabeformate erzeugt. Der Tailwind @theme-Block wird dabei zu einem von mehreren generierten Artefakten, nicht mehr zur einzigen Quelle der Wahrheit.
2. Eine JSON-Token-Datei als Single Source of Truth
Der erste Schritt zu einem funktionierenden Theme-Generator ist die Definition eines konsistenten JSON-Formats für alle Design-Tokens. Eine bewährte Struktur orientiert sich am Community-Vorschlag der W3C Design Tokens Community Group, bei dem jeder Token einen $value und optional einen $type trägt, verschachtelt in einer thematischen Baumstruktur wie color.brand.primary oder spacing.card.padding.
Diese JSON-Datei wird zur einzigen Stelle, an der ein Entwickler oder Designer tatsächlich einen Farbwert ändert. Alle anderen Formate, der Tailwind-@theme-Block, native Mobile-Konstanten oder eine Figma-Sync-Datei, entstehen ausschließlich durch Ausführen des Theme-Generators, niemals durch manuelle Bearbeitung. Das verhindert strukturell, dass generierte Dateien und Quelle auseinanderlaufen, weil es schlicht keinen zweiten Ort mehr gibt, an dem ein Wert manuell geändert werden könnte.
Für Teams mit mehreren Marken oder Themes lässt sich dieselbe JSON-Struktur um eine zusätzliche Ebene erweitern, etwa themes.default.color.brand.primary und themes.dark.color.brand.primary, sodass ein Theme-Generator aus derselben Quelle sowohl den Standard- als auch den Dark-Mode-@theme-Override erzeugen kann.
{
"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. Der Generator-Grundbaustein
Der eigentliche Theme-Generator ist ein kleines Node.js-Skript, das die JSON-Token-Datei einliest, rekursiv über die verschachtelte Struktur läuft und für jeden gefundenen $value-Eintrag eine CSS Custom Property innerhalb eines @theme-Blocks erzeugt. Die Rekursion baut dabei den vollständigen Pfad als Variablennamen zusammen, etwa aus color.brand.primary wird --color-brand-primary.
Der Generator sollte bewusst einfach gehalten werden, ein reines Transformationsskript ohne komplexe Abhängigkeiten, damit es leicht wartbar bleibt und wenig Angriffsfläche für Build-Fehler bietet. Für die meisten Projekte reichen wenige Dutzend Zeilen JavaScript aus, ganz ohne ein schweres Design-Token-Framework als Abhängigkeit, wobei sich für sehr komplexe Multi-Plattform-Setups auch etablierte Tools wie Style Dictionary anbieten, die denselben Grundgedanken bereits vorimplementiert haben.
Ein wichtiges Detail: Der Theme-Generator sollte die generierte Datei mit einem klaren Kommentar-Header kennzeichnen, der davor warnt, die Datei manuell zu bearbeiten, und auf die JSON-Quelle verweist. Das verhindert, dass ein Entwickler versehentlich direkt in der generierten CSS-Datei einen Wert ändert, der beim nächsten Generator-Lauf wieder überschrieben würde.
// 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. Namenskonvention: von JSON-Pfad zu CSS-Variable
Eine konsistente Namenskonvention zwischen JSON-Pfad und CSS-Variable ist die Grundlage jedes wartbaren Theme-Generators. Die einfachste und am leichtesten nachvollziehbare Regel ist eine direkte Eins-zu-eins-Übersetzung: Jede Verschachtelungsebene im JSON wird durch einen Bindestrich getrennt an den Variablennamen angehängt, sodass color.brand.primary zu --color-brand-primary wird, ohne Sonderfälle oder Umbenennungen.
Diese direkte Übersetzung hat einen praktischen Vorteil: Ein Entwickler, der eine CSS-Variable im Code sieht, kann sofort den entsprechenden Pfad in der JSON-Quelle finden, ohne eine separate Mapping-Tabelle nachschlagen zu müssen. Ein Theme-Generator, der stattdessen Variablennamen umbenennt oder abkürzt, etwa --c-b-p statt --color-brand-primary, spart zwar ein paar Zeichen, erschwert aber die Nachvollziehbarkeit erheblich und sollte vermieden werden.
Für Tailwind-spezifische Präfixe wie --color-* für Farben oder --spacing-* für Abstände lohnt sich eine Validierung im Generator selbst, die sicherstellt, dass der oberste JSON-Schlüssel tatsächlich einer der von Tailwind erkannten Namensräume ist. So verhindert der Theme-Generator, dass ein Tippfehler in der JSON-Struktur zu einer Custom Property führt, die von Tailwind zwar generiert, aber von keiner Utility-Klasse jemals konsumiert wird.
5. Mehrere Ausgabeformate aus einer Quelle
Der eigentliche Mehrwert eines Theme-Generators zeigt sich, sobald aus derselben JSON-Quelle mehrere unterschiedliche Ausgabeformate erzeugt werden. Neben dem Tailwind-@theme-Block lässt sich derselbe Token-Baum problemlos in eine Colors.swift-Datei für iOS, eine colors.xml-Ressource für Android oder eine JSON-Datei im Figma-Tokens-Plugin-Format übersetzen, jeweils mit einer eigenen, kleinen Transformationsfunktion, die dieselbe JSON-Quelle als Eingabe nimmt.
Diese Mehrfachausgabe funktioniert am saubersten, wenn der Theme-Generator intern in zwei klar getrennte Schritte aufgeteilt wird: Zuerst das Einlesen und Validieren der JSON-Quelle in eine normalisierte, flache Zwischenrepräsentation, danach eine Reihe unabhängiger Ausgabe-Funktionen, die jeweils nur diese Zwischenrepräsentation konsumieren. Ein neues Ausgabeformat hinzuzufügen, etwa für ein zukünftiges React-Native-Projekt, bedeutet dann lediglich eine weitere Ausgabe-Funktion, ohne den Rest des Generators anzufassen.
Für Design-Teams lohnt sich zusätzlich eine automatisierte Synchronisation mit Figma über dessen REST-API, sodass Designer immer mit denselben Token arbeiten, die auch im produktiven Tailwind-Build ankommen. Der Theme-Generator wird dadurch zur zentralen Nahtstelle zwischen Design und Entwicklung, statt einer reinen Entwickler-internen Build-Hilfe.
6. Validierung der Token-Datei vor der Generierung
Ein Theme-Generator ohne Validierung übernimmt fehlerhafte Werte kommentarlos in alle generierten Ausgaben. Ein Tippfehler wie #0ea5e statt #0ea5e9, ein fehlendes Prozentzeichen bei einem HSL-Wert oder ein leerer $value sollte den Build-Prozess mit einer klaren Fehlermeldung stoppen, statt fehlerhaftes CSS auszuliefern, das erst im Browser aus fällt.
Ein einfacher, aber wirkungsvoller Ansatz ist eine Validierungsfunktion, die vor der eigentlichen Generierung über den Token-Baum läuft und für jeden erkannten $type das Format des zugehörigen $value prüft. Für $type: "color" etwa ein regulärer Ausdruck für gültige Hex-Codes oder RGB-Funktionsnotation, für $type: "dimension" eine Prüfung auf eine gültige CSS-Einheit. Der Theme-Generator bricht bei einem Validierungsfehler mit einem klaren Hinweis auf den betroffenen Token-Pfad ab, statt stillschweigend einen ungültigen Wert weiterzureichen.
Für größere Teams empfiehlt sich zusätzlich ein formales JSON-Schema, das in der CI-Pipeline vor dem eigentlichen Generator-Lauf geprüft wird. Das verschiebt Fehler noch weiter nach vorn im Prozess, idealerweise direkt in den Pull-Request-Check, bevor überhaupt ein Build angestoßen wird.
7. Integration in die Build-Pipeline
Der Theme-Generator muss zuverlässig vor dem eigentlichen Tailwind-Build laufen, damit die generierte @theme-Datei bereits existiert, wenn Tailwind seine Kompilierung startet. In der Praxis bedeutet das ein npm run tokens-Skript, das als pre-Hook vor dem regulären npm run build-Skript automatisch ausgeführt wird, ohne dass Entwickler die Reihenfolge manuell im Kopf behalten müssen.
Für die lokale Entwicklung lohnt sich zusätzlich ein Watch-Modus, der bei jeder Änderung an der JSON-Token-Datei automatisch den Theme-Generator neu ausführt, parallel zum ohnehin laufenden Tailwind-Watch-Prozess. So sieht ein Entwickler eine Farbänderung im JSON praktisch in Echtzeit im Browser, ohne den Build-Prozess manuell neu anstoßen zu müssen.
Für Continuous-Integration-Umgebungen sollte der Generator-Lauf Teil desselben Build-Schritts sein wie die Tailwind-Kompilierung, sodass ein fehlerhafter Token niemals unbemerkt in ein Deployment gelangt. Ein fehlgeschlagener Theme-Generator-Lauf sollte den gesamten Build hart abbrechen, nicht nur eine Warnung ausgeben, die in den CI-Logs übersehen werden könnte.
# 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. Versionierung und Diffing von Token-Änderungen
Weil die generierte @theme-Datei aus einer einzigen JSON-Quelle entsteht, wird ein Git-Diff auf dieser JSON-Datei zur zentralen Stelle, an der Reviewer eine Theme-Generator-Änderung nachvollziehen können. Ein Pull Request, der eine Markenfarbe ändert, zeigt genau eine geänderte Zeile in der JSON-Datei, statt verstreuter Änderungen über mehrere generierte Ausgabedateien hinweg, was Code-Reviews erheblich erleichtert.
Die generierten Ausgabedateien selbst, etwa die kompilierte @theme-CSS-Datei, sollten idealerweise gar nicht ins Versionskontrollsystem eingecheckt werden, sondern als Build-Artefakt behandelt werden, das bei jedem Build frisch entsteht. Das hält das Repository sauber und verhindert Merge-Konflikte in generierten Dateien, die ohnehin nur aus der JSON-Quelle abgeleitet werden.
Für Projekte, in denen die generierte Datei aus praktischen Gründen doch eingecheckt werden muss, etwa weil kein Build-Schritt vor dem Deployment existiert, lohnt sich ein CI-Check, der den Theme-Generator erneut ausführt und das Ergebnis mit der eingecheckten Version vergleicht. Weicht das Ergebnis ab, schlägt der Build fehl, was zuverlässig verhindert, dass jemand versehentlich die generierte Datei manuell bearbeitet, ohne die JSON-Quelle entsprechend anzupassen.
# 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. Manueller Block versus generierter Block im Vergleich
Die folgende Gegenüberstellung zeigt, wann sich der zusätzliche Aufwand eines Theme-Generators gegenüber einem von Hand gepflegten @theme-Block tatsächlich lohnt.
| Kriterium | Manueller @theme-Block | Generierter Block aus JSON | Bewertung |
|---|---|---|---|
| Single Source of Truth | Nur für Web gültig | Gilt plattformübergreifend | Generator klar im Vorteil bei Multi-Plattform |
| Konsistenz mit nativen Apps | Manuelle Synchronisation nötig | Automatisch aus derselben Quelle | Weniger Divergenzrisiko |
| Setup-Aufwand | Keiner, direkt schreibbar | Generator-Skript nötig | Manueller Block schneller für kleine Projekte |
| Validierung von Werten | Keine, Tippfehler bleiben unbemerkt | Zentral im Generator möglich | Generator fängt Fehler früh ab |
| Review-Lesbarkeit | Direkt im CSS sichtbar | Ein Indirektionsschritt über JSON | Beide praktikabel, unterschiedlicher Stil |
Für ein einzelnes Web-Projekt ohne native Apps oder Design-Tool-Synchronisation bleibt ein manuell gepflegter @theme-Block oft die einfachere Wahl. Sobald aber mehr als eine Plattform dieselben Design-Tokens braucht, überwiegt der Aufwand eines Theme-Generators schnell die Kosten der doppelten manuellen Pflege.
Mironsoft
Tailwind CSS v4, Design-Token-Pipelines und Build-Automatisierung
Eine Token-Quelle für Web, App und Figma?
Wir bauen Theme-Generator-Pipelines, die JSON-Design-Tokens automatisch in Tailwind CSS v4, native Mobile-Formate und Figma-Styles übersetzen, inklusive Validierung und CI-Absicherung.
Token-Architektur
JSON-Struktur und Namenskonvention für euer Design-System definieren
Generator-Entwicklung
Multi-Output-Generator mit Validierung und CI-Diff-Check produktionsreif umsetzen
Pipeline-Integration
Watch-Modus, Build-Hooks und Figma-Sync in bestehende Prozesse einbinden
10. Zusammenfassung
Ein Theme-Generator verschiebt Design-Tokens von einem CSS-spezifischen, manuell gepflegten @theme-Block hin zu einer plattformneutralen JSON-Quelle, aus der ein kleines Transformationsskript automatisch alle benötigten Ausgabeformate erzeugt. Die Namenskonvention übersetzt JSON-Pfade direkt und nachvollziehbar in CSS-Variablennamen, während Validierung sicherstellt, dass fehlerhafte Werte den Build-Prozess stoppen, statt unbemerkt in Produktion zu gelangen.
Der eigentliche Gewinn zeigt sich, sobald mehr als eine Plattform dieselben Tokens braucht: Ein Theme-Generator hält Web, native Apps und Design-Tools automatisch synchron, ohne dass ein Team drei separate Farbdefinitionen manuell abgleichen muss. Für Projekte ohne Multi-Plattform-Bedarf bleibt ein einfacher, manueller @theme-Block weiterhin die pragmatischere Wahl.
Theme-Generator für Design-Tokens — Das Wichtigste auf einen Blick
JSON als Single Source
Eine plattformneutrale Token-Datei ersetzt mehrfach gepflegte Farbdefinitionen.
Direkte Namenskonvention
JSON-Pfad und CSS-Variablenname sind identisch aufgebaut, keine versteckten Mappings.
Validierung vor Generierung
Fehlerhafte Werte stoppen den Build, statt still in generiertes CSS zu gelangen.
CI-Diff-Check
Verhindert, dass generierte Dateien manuell bearbeitet werden und aus dem Takt geraten.