Skip to content

Türchen 18 – Theming mit CSS

Published: at 07:00 AM

Theming ist heute ein zentrales Thema in UI-Entwicklung. Light Mode, Dark Mode, High-Contrast-Themes, verschiedene Brand-Varianten oder dynamische User-Einstellungen gehören fast überall zum Standard.

Früher war dafür nahezu zwingend JavaScript notwendig. Heute kann man in modernen Projekten nahezu das gesamte Theming rein in CSS abbilden. Die Basis sind CSS Custom Properties, moderne Farbräume wie OKLCH und Funktionen wie color-mix().

Dieses Türchen erklärt, wie man Theming in CSS professionell aufbaut und skaliert.

Warum CSS für Theming perfekt geeignet ist

Custom Properties haben eine fundamentale Eigenschaft, die sie für Theming prädestiniert. Sie sind zur Laufzeit verfügbar und vererbbar. Im Gegensatz zu Variablen in Präprozessoren wie Sass oder Less, die beim Build zu statischen Werten kompiliert werden, bleiben CSS Custom Properties dynamisch. Das bedeutet, dass sie sich während der Laufzeit ändern können und diese Änderungen sofort im gesamten DOM propagiert werden.

Diese Dynamik ermöglicht es, Themes am Wurzelknoten zu definieren und alle darunterliegenden Komponenten übernehmen die Werte automatisch durch Vererbung. Eine Komponente muss nicht wissen, welches Theme gerade aktiv ist. Sie referenziert einfach var(--color-primary) und erhält automatisch den richtigen Wert, egal ob Light Mode, Dark Mode oder ein beliebiges anderes Theme aktiv ist.

Ein weiterer entscheidender Vorteil ist, dass sich Variablen überschreiben lassen, ohne die CSS-Spezifität zu erhöhen. In traditionellen CSS-Architekturen führte das Überschreiben von Styles oft zu Spezifitätskämpfen, bei denen man immer spezifischere Selektoren schreiben musste. Mit Custom Properties definiert man einfach eine Variable erneut in einem spezifischeren Scope, und sie überschreibt die globale Definition, ohne die Spezifität der eigentlichen Style-Regeln zu berühren.

JavaScript wird bei diesem Ansatz nur noch optional eingesetzt, etwa um das aktive Theme im LocalStorage zu speichern oder ein Data-Attribut am Root-Element zu setzen. Die gesamte visuelle Umschaltung passiert aber rein in CSS. Das Ergebnis ist ein sauberes, komponentenbasiertes Theming-System, das sowohl flexibel als auch robust ist und sich elegant in moderne Frontend-Architekturen einfügt.

Schritt 1: Ein zentrales Token-System definieren

Jedes professionelle Theme-System beginnt mit einer sauberen Architektur von Design Tokens. Diese Tokens sind nicht einfach nur Variablen, sondern semantische Abstraktionen, die die Designsprache des Produkts definieren. Sie fungieren als Single Source of Truth für alle visuellen Eigenschaften und sorgen dafür, dass Änderungen zentral vorgenommen werden können, ohne jeden einzelnen Selektor anfassen zu müssen.

:root {
  /* Farbwerte in OKLCH */
  --color-bg: oklch(0.98 0.01 0);
  --color-text: oklch(0.2 0.02 260);

  /* Abstände */
  --space-s: 0.5rem;
  --space-m: 1rem;
  --space-l: 2rem;

  /* Radius */
  --radius: 0.5rem;

  /* Typografie */
  --fs-base: clamp(1rem, 0.5vw + 0.9rem, 1.125rem);
}

Die Verwendung von OKLCH für Farbwerte ist dabei kein Zufall. Dieser moderne Farbraum ist wahrnehmungslinear, was bedeutet, dass numerische Änderungen direkt mit der visuellen Wahrnehmung korrelieren. Das macht es deutlich einfacher, harmonische Farbpaletten zu erstellen und zwischen Light und Dark Mode zu wechseln, ohne dass Farben ihre visuelle Identität verlieren.

Die Abstände folgen einer klaren Skala, die sich durch das gesamte Interface zieht und für vertikalen wie horizontalen Rhythmus sorgt. Der Radius-Wert definiert die Grundform aller abgerundeten Elemente und trägt maßgeblich zur visuellen Identität bei. Die typografische Basis nutzt clamp() für fluide Skalierung, sodass Schriftgrößen sich organisch an verschiedene Viewport-Größen anpassen, ohne harte Breakpoints zu benötigen.

Diese Werte bilden das Fundament eines konsistenten Themes und werden von allen Komponenten referenziert, wodurch eine kohärente visuelle Sprache entsteht.

Schritt 2: Light & Dark Theme über einfache CSS-Scopes

Die Implementierung von Light und Dark Mode wird durch CSS Custom Properties elegant gelöst. Statt separate Stylesheets oder komplexe JavaScript-Logik zu benötigen, definiert man einfach unterschiedliche Werte für dieselben Variablen in verschiedenen Scopes. Der entscheidende Vorteil: Die Komponenten müssen nichts über das aktive Theme wissen, sie referenzieren einfach die Variablen, und diese liefern automatisch die richtigen Werte.

/* Light Theme */
:root {
  --color-bg: oklch(0.98 0.01 0);
  --color-text: oklch(0.2 0.01 260);
  --color-primary: oklch(0.62 0.16 260);
}

/* Dark Theme */
[data-theme="dark"] {
  --color-bg: oklch(0.18 0.01 0);
  --color-text: oklch(0.95 0.02 260);
  --color-primary: oklch(0.75 0.16 260);
}

Das Pattern ist bemerkenswert einfach. Im Light Mode hat der Hintergrund eine sehr hohe Helligkeit (0.98), während der Text dunkel ist (0.2). Im Dark Mode invertieren sich diese Werte: Der Hintergrund wird dunkel (0.18), der Text hell (0.95). Entscheidend ist, dass Chroma (Sättigung) und Hue (Farbton) identisch bleiben. Nur die Helligkeit ändert sich, was dafür sorgt, dass die Farbidentität erhalten bleibt und das Theme konsistent wirkt.

Die Verwendung in Komponenten ist denkbar simpel:

body {
  background: var(--color-bg);
  color: var(--color-text);
}
.button {
  background: var(--color-primary);
}

Dieser Ansatz vermeidet jegliche Spezifitätsprobleme, weil man nicht mit Selektoren wie .dark-mode .button arbeiten muss. Es gibt keine redundanten Klassen, die an jedes Element gehängt werden müssen. Stattdessen funktioniert alles über reines var()-basiertes Styling, das sich elegant durch das gesamte DOM vererbt. Ein einziges Data-Attribut am Root-Element reicht aus, um das gesamte Interface umzuschalten.

Schritt 3: Systembasiertes Theming (prefers-color-scheme)

Eine der elegantesten Features für modernes Theming ist die Möglichkeit, automatisch auf die Systemeinstellungen des Nutzers zu reagieren. Die prefers-color-scheme Media Query erlaubt es, zu erkennen, ob ein Nutzer auf Betriebssystemebene Light oder Dark Mode bevorzugt, und das Theme entsprechend anzupassen. Das respektiert die Nutzerpräferenzen und sorgt dafür, dass die Website sich nahtlos in die gewohnte Umgebung einfügt.

Die Implementierung kann auf verschiedene Weisen erfolgen. Der einfachste Ansatz setzt eine Variable, die dann von anderen Regeln verwendet werden kann:

@media (prefers-color-scheme: dark) {
  :root {
    --theme-mode: dark;
  }
}

Alternativ kann man die Theme-Farben direkt in der Media Query definieren:

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: oklch(0.18 0.01 0);
    --color-text: oklch(0.95 0.02 260);
    --color-primary: oklch(0.75 0.16 260);
  }
  
  html {
    color-scheme: dark;
  }
}

Dieser Ansatz kombiniert beide Strategien. Die Theme-Variablen werden automatisch angepasst und die color-scheme-Eigenschaft signalisiert dem Browser, dass die Seite für Dark Mode optimiert ist. Das hat weitreichende Auswirkungen auf native Browser-Elemente. Form Controls wie Checkboxen, Radio Buttons und Select-Felder passen sich automatisch an und werden in ihrer dunklen Variante dargestellt. Scrollbars erhalten ebenfalls ein dunkleres Erscheinungsbild, das besser zum Theme passt. Input-Felder bekommen automatisch angepasste Hintergrund- und Textfarben, ohne dass man diese manuell definieren muss. Auch Hintergrundschattierungen, die der Browser für bestimmte Elemente hinzufügt, werden automatisch an das dunkle Theme angepasst.

Diese automatische Anpassung spart nicht nur viel Code, sondern sorgt auch dafür, dass native Elemente konsistent mit dem Rest der Seite wirken, ohne dass man jedes einzelne Element manuell stylen muss.

JavaScript-Integration (optional): Wenn Nutzer manuell zwischen Themes wechseln sollen, reicht eine einzige Zeile JavaScript:

// Theme umschalten
document.documentElement.dataset.theme = 'dark';

// Theme aus LocalStorage laden
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
  document.documentElement.dataset.theme = savedTheme;
}

Dieser Code setzt das data-theme-Attribut am <html>-Element, und CSS übernimmt automatisch den Rest.

Schritt 4: Brand Themes – dynamische Farbwelten

Brand Themes sind eine der mächtigsten Anwendungen von CSS-Theming. Sie ermöglichen es, mit minimalem Code-Overhead komplett unterschiedliche visuelle Identitäten zu unterstützen. Statt für jeden Brand separate Stylesheets zu pflegen, definiert man einfach unterschiedliche Werte für dieselben Variablen. Der Rest des Systems bleibt vollständig unverändert.

Die Kapselung erfolgt über Data-Attribute, die flexibel auf verschiedenen Ebenen des DOM gesetzt werden können:

[data-brand="blue"] {
  --brand: oklch(0.62 0.15 250);
}

[data-brand="green"] {
  --brand: oklch(0.65 0.14 140);
}

Alle Komponenten, die die Brand-Farbe verwenden, referenzieren einfach die Variable:

.button {
  background-color: var(--brand);
}

Sobald das Data-Attribut data-brand="green" am Root-Element oder an einem Container gesetzt wird, übernehmen alle darunterliegenden Elemente automatisch die grüne Brand-Farbe. Es ist kein JavaScript erforderlich, keine komplexe Logik, nur das Setzen eines einzigen Attributs.

Dieser Ansatz ist besonders wertvoll für Produkte, die verschiedene Markenidentitäten unterstützen müssen. White-Label-UIs, bei denen dasselbe Produkt unter verschiedenen Markennamen mit jeweils eigener visueller Identität verkauft wird, lassen sich so mit minimalem Aufwand realisieren. Mandanten-Designsysteme in B2B-Software, wo jeder Kunde sein eigenes Corporate Design haben möchte, können elegant über Theme-Variablen abgebildet werden. Auch Multi-Brand-Produkte, bei denen ein Unternehmen mehrere Marken unter einem technischen Dach betreibt, profitieren von diesem Pattern, da sich die Brand-Identity rein über CSS-Variablen steuern lässt, ohne den zugrunde liegenden Code zu verändern.

Schritt 5: Farbvarianten über color-mix() generieren

Ein entscheidender Vorteil moderner CSS-Theming-Systeme ist die Fähigkeit, Farbvarianten dynamisch aus Basis-Farben zu generieren. Die color-mix()-Funktion macht es möglich, komplette Farbskalen zu erstellen, ohne jede einzelne Abstufung manuell definieren zu müssen. Das reduziert nicht nur den Code-Overhead, sondern sorgt auch dafür, dass alle Varianten automatisch harmonisch aufeinander abgestimmt sind.

:root {
  --brand: oklch(0.62 0.16 260);

  --brand-100: color-mix(in oklch, var(--brand) 20%, white);
  --brand-200: color-mix(in oklch, var(--brand) 40%, white);
  --brand-300: color-mix(in oklch, var(--brand) 60%, white);

  --brand-600: color-mix(in oklch, var(--brand) 80%, black);
  --brand-700: color-mix(in oklch, var(--brand) 60%, black);
}

Das Mischen erfolgt im OKLCH-Farbraum, was entscheidend ist. Im Gegensatz zu RGB oder HSL sind Mischungen in OKLCH wahrnehmungslinear und erzeugen natürlichere, harmonischere Übergänge. Die helleren Varianten (100, 200, 300) entstehen durch Mischung mit Weiß, die dunkleren (600, 700) durch Mischung mit Schwarz. Das Ergebnis ist eine vollständige Farbskala, die harmonisch und konsistent ist – und das ohne manuelle Farbwahl oder Design-Tools.

Besonders wertvoll ist dieser Ansatz für dynamische Themes. Ändert sich die Basis-Farbe --brand, passen sich automatisch alle abgeleiteten Varianten an. Das ist ideal für White-Label-Produkte oder Themes, bei denen Nutzer ihre eigenen Farben definieren können.

Schritt 6: Komponenten-themenfähig machen

Die Kunst eines guten Theme-Systems liegt darin, Komponenten sowohl global themebar als auch lokal überschreibbar zu machen. Das Pattern, das sich dafür bewährt hat, nutzt komponentenspezifische Variablen mit Fallback-Werten. Dadurch können Komponenten an das globale Theme gekoppelt werden, erlauben aber gleichzeitig lokale Anpassungen ohne Spezifitätsprobleme.

.card {
  background: var(--card-bg, var(--color-bg));
  color: var(--card-text, var(--color-text));
  border-radius: var(--radius);
}

Die Syntax var(--card-bg, var(--color-bg)) definiert einen Fallback-Mechanismus. Ist --card-bg nicht definiert, wird --color-bg verwendet. Das bedeutet: Standardmäßig verhält sich die Card wie alle anderen Komponenten und übernimmt die globalen Theme-Farben.

Sobald man aber eine spezifische Variante benötigt, setzt man einfach die komponenteneigenen Variablen:

.card--highlight {
  --card-bg: var(--brand-100);
  --card-text: var(--brand-700);
}

Dieser Modifier überschreibt lokal die Card-Variablen, ohne die globale Struktur zu berühren. Das ist extrem flexibel, weil man mit einer einzigen Variablendeklaration das gesamte Erscheinungsbild der Komponente ändern kann. Gleichzeitig ist es speicherarm und performant, weil keine redundanten Styles definiert werden müssen und die Vererbung automatisch funktioniert.

Schritt 7: Theming ohne Spezifität – mit Layers

In größeren Projekten wird die Kontrolle über CSS-Spezifität zunehmend zur Herausforderung. Verschiedene Entwickler fügen Styles hinzu, Komponenten-Bibliotheken bringen eigene Styles mit und Overrides müssen immer spezifischer werden. CSS Cascade Layers lösen dieses Problem elegant, indem sie eine explizite Hierarchie definieren, die unabhängig von der Spezifität der Selektoren funktioniert.

Für Theme-Systeme empfiehlt sich eine klare Layer-Struktur:

@layer reset, theme, components, overrides;

@layer theme {
  :root {
    --color-bg: oklch(0.98 0.01 0);
  }

  [data-theme="dark"] {
    --color-bg: oklch(0.18 0.01 0);
  }
}

Diese Layer-Hierarchie definiert die Priorität:

Der entscheidende Vorteil: Ein Style im overrides-Layer überschreibt immer einen Style im theme-Layer, unabhängig von der Spezifität der Selektoren. Man muss nicht zu Tricks wie erhöhter Spezifität oder !important greifen.

CSS Cascade Layers bringen Ordnung in die Spezifitätshierarchie von Theme-Definitionen. Durch die explizite Layer-Struktur bleiben Theme-Definitionen stabil, weil sie in einer definierten Schicht des Cascade liegen und nicht durch zufällige Spezifitätsunterschiede überschrieben werden können. Gleichzeitig bleiben sie gezielt überschreibbar, denn Layers mit höherer Priorität (wie der overrides-Layer) können bewusst Theme-Werte überschreiben, ohne dass man zu !important greifen oder die Spezifität künstlich erhöhen muss. Die Struktur bleibt klar und nachvollziehbar, weil die Layer-Hierarchie explizit am Anfang des Stylesheets deklariert wird und damit für jeden Entwickler sofort ersichtlich ist, welche Styles in welcher Reihenfolge wirken.

Schritt 8: High-Contrast Themes

Barrierefreiheit ist heute kein Nice-to-have mehr, sondern eine grundlegende Anforderung an moderne Webprodukte. High-Contrast Themes sind ein essenzieller Bestandteil dieser Strategie und müssen von Anfang an im Theme-System mitgedacht werden. Sie richten sich an Menschen mit Sehbeeinträchtigungen, aber auch an Nutzer in schwierigen Lichtverhältnissen oder mit bestimmten kognitiven Einschränkungen.

[data-theme="high-contrast"] {
  --color-bg: oklch(1 0 0);
  --color-text: oklch(0 0 0);
  --color-primary: oklch(0.85 0.25 20);
}

Die Farbdefinitionen sind bewusst extrem gewählt. Der Hintergrund ist reines Weiß (Lightness 1, Chroma 0), der Text reines Schwarz (Lightness 0, Chroma 0). Das ergibt ein maximales Kontrastverhältnis von 21:1, was deutlich über den WCAG AAA-Standard hinausgeht. Die Primary-Farbe hat eine hohe Helligkeit (0.85) und eine kräftige Sättigung (0.25), sodass sie auf weißem Hintergrund deutlich sichtbar bleibt und trotzdem Farbinformation vermittelt.

High-Contrast Themes sind essenziell für Barrierefreiheit und ermöglichen es, Kontraste gezielt zu steigern, sodass auch Menschen mit Sehbeeinträchtigungen oder in schwierigen Lichtverhältnissen alle Inhalte problemlos erfassen können. Durch gezielte Reduzierung der Farbsättigung werden ablenkende Farbeffekte minimiert, was besonders für Menschen mit bestimmten kognitiven Einschränkungen oder Farbsehschwächen hilfreich ist. UI-Elemente lassen sich durch klarere Farbkontraste deutlicher hervorheben, sodass interaktive Bereiche sofort erkennbar sind und die Bedienbarkeit verbessert wird.

Solche Themes sind ideal für die Erfüllung der WCAG-Richtlinien auf AA- oder AAA-Niveau, da sie die geforderten Kontrastverhältnisse deutlich übertreffen und damit einen barrierefreien Zugang für ein breites Spektrum von Nutzerinnen und Nutzern gewährleisten.

Praxisbeispiel: Vollständiges Setup für Light/Dark/Brand

Ein vollständiges, produktionsreifes Theme-System muss nicht komplex sein. Im Gegenteil: Die Eleganz liegt in der Einfachheit. Das folgende Beispiel zeigt, wie man mit minimalem Code ein Theme-System aufbaut, das Light Mode, Dark Mode und verschiedene Brand-Varianten unterstützt und dabei vollständig wartbar und erweiterbar bleibt.

:root {
  --surface: oklch(0.98 0.01 0);
  --text: oklch(0.2 0.02 260);
  --brand: oklch(0.62 0.16 260);
}

[data-theme="dark"] {
  --surface: oklch(0.18 0.01 0);
  --text: oklch(0.95 0.02 260);
  --brand: oklch(0.75 0.16 260);
}

[data-brand="green"] {
  --brand: oklch(0.65 0.14 140);
}

.button {
  background: var(--brand);
  color: var(--text);
}

.card {
  background: var(--surface);
  color: var(--text);
}

Dieses Setup ist bewusst minimalistisch gehalten, aber es funktioniert. Die Basis-Tokens definieren Surface (Hintergrundflächen), Text und Brand (Markenfarbe). Der Dark Mode überschreibt diese Tokens mit invertierten Helligkeitswerten, während Chroma und Hue konsistent bleiben. Brand-Varianten können separat definiert werden und überschreiben nur die Brand-Farbe, während alle anderen Werte unverändert bleiben.

Das Ergebnis ist ein vollständiges Theme-System in unter 20 Zeilen CSS, das robust funktioniert, leicht erweiterbar ist und sich nahtlos in jede bestehende Architektur integrieren lässt. Komponenten wie Buttons und Cards referenzieren einfach die Tokens und übernehmen automatisch das aktive Theme, ohne eigene Theme-Logik implementieren zu müssen.

Browser-Support und Performance

Browser-Unterstützung: Alle modernen Browser unterstützen die vorgestellten Techniken sehr gut. CSS Custom Properties funktionieren in allen Browsern außer Internet Explorer 11. Die color-scheme-Eigenschaft und prefers-color-scheme Media Query werden von Chrome 76+, Firefox 67+, Safari 12.1+ und Edge 79+ unterstützt. Die color-mix()-Funktion ist in Chrome 111+, Firefox 113+ und Safari 16.2+ verfügbar. CSS Cascade Layers sind ab Chrome 99+, Firefox 97+ und Safari 15.4+ nutzbar. Für ältere Browser bieten sich Fallbacks mit statischen Farben an.

Performance: CSS Custom Properties haben einen minimalen Performance-Overhead, der in der Praxis nicht messbar ist. Der Browser cached die Werte effizient und berechnet sie nur bei Änderungen neu. Im Vergleich zu JavaScript-basierten Theming-Lösungen ist der CSS-Ansatz deutlich performanter, da keine DOM-Manipulationen oder Re-Renderings erforderlich sind. Das gesamte Theming läuft im CSS-Engine des Browsers und ist damit optimal optimiert. Selbst bei komplexen Theme-Systemen mit Hunderten von Variablen bleibt die Performance exzellent.

Fazit

Modernes CSS-Theming hat sich zu einem robusten und mächtigen Werkzeug entwickelt, das ohne JavaScript auskommt und dennoch höchst flexible Theme-Systeme ermöglicht. Im Kern basiert dieser Ansatz auf CSS Custom Properties, die als dynamische Variablen zur Laufzeit verfügbar sind und sich elegant vererben lassen. Die Integration moderner Farbräume wie OKLCH sorgt dafür, dass Farben wahrnehmungslinear skalieren und sich konsistent zwischen Light und Dark Mode transformieren lassen, ohne dass die visuelle Identität verloren geht.

Die color-scheme-Eigenschaft kommuniziert mit dem Browser und sorgt dafür, dass native Elemente automatisch zum aktiven Theme passen. Die prefers-color-scheme Media Query ermöglicht es, auf Systemeinstellungen des Nutzers zu reagieren und das Theme automatisch anzupassen. Die color-mix()-Funktion erlaubt es, Farbvarianten dynamisch aus Basis-Farben zu generieren, ohne dass man jede Abstufung manuell definieren muss. All diese Techniken fügen sich zu einem klaren Token-System zusammen, das als Single Source of Truth für alle visuellen Eigenschaften dient.

Das Ergebnis ist beeindruckend. Light und Dark Themes lassen sich mit minimalem Code-Overhead implementieren, Brand Themes können flexibel für verschiedene Mandanten oder Marken definiert werden und spezielle Varianten wie High-Contrast Themes für Barrierefreiheit sind problemlos integrierbar. Die Wartbarkeit ist exzellent, weil Änderungen zentral an einer Stelle vorgenommen werden und sich automatisch durch das gesamte System propagieren. Und das alles funktioniert robust und performant, ohne dass JavaScript für die eigentliche Theme-Logik benötigt wird.


☕ Buy me a coffee

Wenn Dir meine Beiträge gefallen und sie Dir bei Deiner Arbeit helfen, würde ich mich über einen "Kaffee" und ein paar nette Worte von Dir freuen.

Buy me a coffee

Previous Post
Türchen 19 – Transitions und Microinteractions
Next Post
Türchen 17 – CSS Color Level 5

Kommentare