Design Tokens – Design smarter, not harder
Design Tokens schaffen eine gemeinsame Sprache für Gestaltung und Programmierung. Sie strukturieren Farben, Rollen und Komponenten in einem nachvollziehbaren System und machen Designs flexibel und anpassbar. Wir zeigen, wie sich ein solches Token-System in drei Schichten aufbauen lässt und wie öffentliche und private Tokens für anpassbare Komponenten sorgen.
In diesem Beitrag
Was ist ein Design Token?
Vorweg: Design Tokens sind keine technische Neuerung in CSS, sondern nur ein Ansatz, mit Variablen umzugehen.
Ein Design Token ist eine benannte Gestaltungsentscheidung. Statt oklch(0.5161 0.2019 25.94) schreiben wir --color-accent. Statt 0.375rem steht dort --border-radius-m.
Technisch sind Design Tokens schlicht CSS Custom Properties. Anders als SCSS-Variablen werden sie nicht beim Kompilieren ersetzt, sondern erst im Browser ausgewertet. Derselbe Token kann deshalb je nach Kontext einen anderen Wert haben. Genau darauf baut später unser Theme-Konzept auf.
Entscheidend ist also nicht die Technik, sondern *wie* wir die Tokens benennen und in welche Schichten wir sie aufteilen.
Welche Probleme lösen sie?
Ohne Tokens passiert in jedem gewachsenen Projekt dasselbe:
- Derselbe Wert steht an vielen Stellen. Ändert sich die Markenfarbe, beginnt die Suche mit „Suchen und Ersetzen“, und irgendwo bleibt ein alter Wert stehen.
- Kontexte brauchen Sonderregeln. Der Footer ist dunkel, also bekommt der Button dort einen eigenen Selektor oder diese eine besondere Card auf der Startseite soll hervorstehen, usw.
- Anpassungen werden zum Spezifitätskampf. Wer eine Komponente leicht verändern will, muss deren Selektoren überbieten.
- Design und Code sprechen verschiedene Sprachen. Im Design heißt es „die Akzentfarbe“, im Code steht ein Hexwert. Tokens geben beiden Seiten dieselben Begriffe und erleichtern so die Zusammenarbeit zwischen Designer:innen und Entwickler:innen.
Ein schöner Nebeneffekt: Es gibt weniger Bauch-Entscheidungen. Wer einen Token wählen muss, fragt sich automatisch, welche Rolle ein Wert eigentlich hat. Man bekommt ein fertiges Design vom Kunden gestellt. Beim Übersetzen in Tokens fallen dann Stellen auf, an denen das Design selbst evtl. nicht ganz konsequent ist.
Design Tokens lösen diese Probleme, indem sie Werte, Bedeutungen und Bauteile voneinander trennen. Bei uns sind das drei Schichten. Ähnlich wie im ITCSS-Schichtenmodell, nach dem auch unsere SCSS-Ordner aufgebaut sind, werden sie von unten nach oben immer spezifischer:
- Primitive Token - „Welcher Wert?“ -
--palette-red-500 - Decision Token - „Welche Rolle?“ -
--color-accent - Component Token - „Welche Eigenschaft welcher Komponente?“ -
--button-color-background-idle
Die Regel: Jede Schicht verweist nur auf die Schicht darunter. Eine Komponente kennt keine Palettenfarbe, nur Rollen.
Schicht 1: Primitive Token
Primitive Token beschreiben, was ein Wert ist, nicht wofür er verwendet wird. Sie stehen einmal global in :root:
:root {
--palette-gray-100: oklch(1.000 0.000 0);
--palette-gray-900: oklch(0.000 0.000 0);
--palette-sepia-100: oklch(0.9584 0.0381 85.35);
--palette-sepia-200: oklch(0.9234 0.0558 86.64);
--palette-blue-800: oklch(0.2938 0.0656 238.47);
--palette-blue-900: oklch(0.1938 0.0656 238.47);
--palette-red-400: oklch(0.6161 0.2019 25.94);
--palette-red-500: oklch(0.5161 0.2019 25.94);
}Das Schema ist --palette-<farbe>-<stufe>. Die Stufen reichen, angelehnt an die Gewichte von font-weight, von 100 (hell) bis 900 (dunkel).
Langweilige Namen sind gute Namen
Für Farbnamen gibt es Generatoren wie Name That Color. Die liefern allerdings Namen wie „San Juan“. Das scheint im ersten Moment schnell und einfach zu sein, allerdings verrät dieser Name nicht den Farbton. Kolleg:innen müssen dann jedes Mal nachschauen, welche Farbe dahintersteckt, oder man selbst hat nach ein paar Wochen keine Ahnung mehr. Naming things is hard … Deshalb lieber eindeutige, etwas langweilige Namen wie --palette-blue-800.
Regel: Komponenten verwenden Palettenfarben nie direkt.
Schicht 2: Decision Token
In der zweiten Schicht fällt die eigentliche Gestaltungsentscheidung: Welche Palettenfarbe übernimmt welche Rolle? Anders als Primitive und Component Tokens beschreiben Decision Tokens, welche Bedeutung ein Wert im Kontext der Anwendung hat. Deshalb sind sie auch am schwersten zu benennen. Dazu später mehr.
Diese Tokens werden nicht global, sondern pro Theme belegt:
[data-theme="default"] {
--color-background: var(--palette-gray-100);
--color-foreground: var(--palette-blue-900);
--color-surface: var(--palette-sepia-100);
--color-accent: var(--palette-red-500);
--color-on-accent: var(--palette-gray-100);
}
[data-theme="inverted"] {
--color-background: var(--palette-blue-900);
--color-foreground: var(--palette-gray-100);
--color-surface: var(--palette-blue-800);
--color-accent: var(--palette-red-400);
--color-on-accent: var(--palette-gray-100);
}Die Namen folgen dem Muster --color-<rolle>[-<variante>], mit Varianten wie -muted, -raised oder -disabled.
Foreground und Background
Nach einigem Ausprobieren hat sich ein einfacher Gedanke bewährt: Jedes Element besteht aus einer Fläche und dem, was darauf liegt.
- Background ist immer eine Hintergrundfläche.
- Foreground ist alles, was auf dieser Fläche steht, also in den meisten Fällen Text und Icons.
Accent und On-Accent
--color-accent kann als Foreground oder Background verwendet werden, sodass ein --color-on-accent benötigt wird. Dadurch kann immer ein außreichender Kontrast sichergestellt werden.
Ebenen: Canvas, Background, Surface
Für Flächen reicht ein einziger Hintergrund schnell nicht aus. Statt „Hintergrundfarbe 1, 2, 3“ denken wir in Ebenen, die übereinanderliegen:
┌─ Seite ─────────────────────────────────────────┐ │ --color-canvas · äußerste Fläche │ │ ┌─ Inhaltsbereich ────────────────────────────┐ │ │ │ --color-background · Standardhintergrund │ │ │ │ ┌─ Termin, Badge, Popover ────────────────┐ │ │ │ │ │ --color-surface · darüberliegende Ebene │ │ │ │ │ │ ┌─ hervorgehobener Hinweis ───────────┐ │ │ │ │ │ │ │ --color-surface-raised │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘
- Canvas ist die äußerste Fläche, die Leinwand, auf der alles liegt. In manchen Fällen ist sie identisch mit dem Background. Die Trennung gibt uns aber die Möglichkeit, etwa auf Landing Pages nur die Leinwand umzufärben.
- Background ist der Standardhintergrund, auf dem der Inhalt steht.
- Surface ist eine Ebene darüber, etwa für eine Card, eine Badge oder ein Popover. Mit
-raisedgeht es noch eine Stufe höher, zum Beispiel für ein Buchungshinweis auf einer Card.
Der Vorteil: Eine Komponente fragt nicht „Bin ich beige?“, sondern „Ich liege eine Ebene über dem Hintergrund“. Im hellen Theme ist diese Ebene ein warmes Sepia, im dunklen ein etwas helleres Blau. Die Komponente merkt davon nichts.
Themes als data attribute
Im Template wird das Theme als data attribute vergeben:
<body data-theme="default">
…
<footer data-theme="inverted">…</footer>
</body>Jedes Element mit einem data-theme setzt dabei selbst Vordergrund und Hintergrund. Ein Theme-Wechsel ist also auch ohne weitere Komponenten sofort sichtbar.
Schicht 3: Component Token
Die dritte Schicht macht aus einer Komponente ein konfigurierbares Bauteil. Component Tokens lassen sich in der Regel deutlich leichter benennen als Decision Tokens. Bei uns lautet das Schema:
--button - color - background - hover │ │ │ └─ Zustand: idle | hover | focus | disabled │ │ └─ Eigenschaft: foreground | background | border … │ └─ Typ: color, border, font … └─ Komponente
Die Komponente steht bewusst vorne. Wer später durch die Tokens scrollt, sieht sofort, zu welcher Komponente ein Token gehört. Den Grundzustand nennen wir ausdrücklich idle, damit jeder Zustand einen Namen hat.
Der Fallback-Trick
Custom Properties können einen Fallback haben. Gedacht ist das für den Fall, dass eine Variable gar nicht gesetzt ist:
color: var(--color-foreground, black);
Fehlt --color-foreground, ist der Text eben schwarz. Wir nutzen diese Funktion etwas anders: Wir lassen den öffentlichen Token absichtlich leer und setzen nur den Fallback. Am Beispiel des Buttons sieht das so aus:
@mixin button() {
--_button-color-foreground-idle: var(--button-color-foreground-idle, var(--color-on-accent));
--_button-color-background-idle: var(--button-color-background-idle, var(--color-accent));
color: var(--_button-color-foreground-idle);
background-color: var(--_button-color-background-idle);
}Jede Zeile liest man am besten von rechts nach links:
1. Fallback: der Decision Token ( --color-accent), also der passende Standard für das aktuelle Theme.
2. Öffentlicher Token ( --button-…): Den setzt die Komponente selbst nie. Er ist dafür da, von außen gesetzt zu werden.
3. Privater Token ( --_button-…, mit Unterstrich): Den liest nur die Komponente selbst.
Der Unterstrich ist eine Namenskonvention, wie man sie aus JavaScript kennt. Technisch privat ist der Token damit natürlich nicht. Er signalisiert aber deutlich: Das hier ist Innenleben, bitte nicht anfassen.
Die CSS-Eigenschaften lesen ausschließlich die privaten Tokens. So ist im Code immer klar, was Schnittstelle ist und was Implementierungsdetail.
Anpassen ohne Selektorkampf
Den Satz kennt jede:r aus dem Projektalltag: „Genau dieser Button soll aber dunkelblau statt rot sein.“ Weil der öffentliche Token ( --button-…) unbelegt ist, kann man ihn einfach setzen, ohne über Spezifität oder Vererbung nachzudenken:
.button--spezial {
--button-color-background-idle: var(--color-background-inverted);
}Auch hier bleibt die Regel gewahrt: Der Sonderfall nimmt eine Rolle, keine Palettenfarbe.
Varianten bringen ebenfalls keine eigenen Regeln mit, sondern belegen nur Tokens neu:
@mixin button-transparent() {
@include button;
--button-color-foreground-idle: currentColor;
--button-color-background-idle: transparent;
}Unsere Empfehlung
Ehrlich gesagt: Ein solches System aufzusetzen kostet am Anfang viel Brain. Man diskutiert über Namen und darüber, welche Eigenschaften eine Komponente überhaupt nach außen anbieten soll. Das fühlt sich zunächst langsamer an, als einfach eine Farbe in den Button zu schreiben.
Die meisten Probleme hatten wir anfangs mit den Decision Tokens. Primitive sind schnell definiert, Component Tokens ergeben sich quasi aus der Komponente. Aber welche Rollen braucht das System wirklich? Ist das „surface“ oder „background“? Braucht es ein -raised oder reicht eine zweite Surface? Die Schwierigkeit ist, die Balance zwischen allgemein und spezifisch zu finden. Genau hier entscheidet sich, ob das System später Arbeit erleichtert.
Es lohnt sich sehr, bei anderen Design Systemen nach Inspiration zu suchen. Eine Kopiervorlage, die man einfach auf ein Projekt wirft, gibt es aber nicht. Am meisten lernt man über die eigenen Systeme und Strukturen, wenn man nicht einfach etwas kopiert, sondern selbst ein passendes Token-System baut. Erst dabei wird sichtbar, welche Rollen das eigene Design tatsächlich hat und wo bisher nur Gewohnheit war.
Unter dem Strich lohnt es sich aber deutlich. Ein neues Theme besteht aus einer Handvoll Zeilen. Eine Farbänderung passiert an genau einer Stelle. Komponenten lassen sich anpassen, ohne dass jemand ihre Selektoren kennen muss. Und wer neu ins Projekt kommt, versteht an den Namen, was ein Wert bedeutet, nicht nur, wie er aussieht.
Unser Rat: Klein anfangen, mit einer Farbpalette und einer Handvoll Rollen. Dann die erste Komponente, beispielsweise eine Card, sauber mit öffentlichen und privaten Tokens aufbauen. Der Button eignet sich dafür weniger, da er durch zahlreiche Sonderregeln, etwa für Hover- und Fokuszustände, komplexer ist. Der Rest folgt fast von selbst.
In diesem Sinne: Ran an die Tokens! 😉