Dark Mode korrekt implementieren, ohne Flash of wrong theme
prefers-color-scheme ist mehr als eine Media Query für Dunkel und Hell. Die color-scheme Property, ein sauberer Nutzer-Override und die richtige Reihenfolge beim ersten Render entscheiden darüber, ob Dark Mode nahtlos wirkt oder mit einem sichtbaren Aufblitzen des falschen Themes startet.
Inhaltsverzeichnis
- 1. Was prefers-color-scheme ist und woher der Wert kommt
- 2. Media Query Syntax: light, dark und die veraltete no-preference
- 3. Die color-scheme Property und native Formularelemente
- 4. Custom Properties automatisch je Farbschema umschalten
- 5. Nutzer-Override trotz Betriebssystem-Einstellung
- 6. Testing in DevTools und Regressions-Erkennung
- 7. Flash of wrong theme vermeiden bei SSR
- 8. Kombination mit prefers-contrast und forced-colors
- 9. Implementierungsansätze im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was prefers-color-scheme ist und woher der Wert kommt
prefers-color-scheme ist eine Media Query, die eine einzige Frage beantwortet: Bevorzugt das Betriebssystem des Nutzers ein helles oder ein dunkles Farbschema. Der Wert stammt nicht von der Webseite selbst, sondern wird global auf Betriebssystemebene eingestellt, etwa in den Systemeinstellungen von macOS, Windows, Android oder iOS, und gilt dort für alle Anwendungen gleichzeitig, die diese Einstellung respektieren.
Für Webseiten bedeutet das: Mit prefers-color-scheme lässt sich ein Dark Mode implementieren, der automatisch zur restlichen Betriebssystemumgebung des Nutzers passt, ohne dass ein eigener Toggle gebaut werden muss. Das ist der wichtigste Unterschied zu einem manuell implementierten Theme-Switch: Die Einstellung kommt von außen, aus einer bereits getroffenen Nutzerentscheidung, statt von einer neuen Entscheidung, die erst auf der jeweiligen Webseite getroffen werden muss.
2. Media Query Syntax: light, dark und die veraltete no-preference
Die Syntax von prefers-color-scheme ist unkompliziert: @media (prefers-color-scheme: dark) { ... } greift, wenn das Betriebssystem ein dunkles Schema bevorzugt, @media (prefers-color-scheme: light) { ... } greift beim hellen Schema. Ein dritter möglicher Wert, no-preference, wurde aus der Spezifikation entfernt, weil sich in der Praxis zeigte, dass Betriebssysteme fast immer eine explizite Präferenz liefern und der Wert kaum genutzt wurde.
Ein häufiger Anfängerfehler: Entwickler bauen zuerst ein komplettes helles Theme und ergänzen dann eine prefers-color-scheme: dark Media Query mit Überschreibungen für jede einzelne Farbe. Das führt zu doppeltem Pflegeaufwand und leicht auseinanderlaufenden Farbwerten. Der robustere Ansatz definiert Custom Properties als Zwischenschicht und überschreibt nur diese innerhalb der Media Query, statt jede einzelne CSS-Regel zu duplizieren.
/* Naive approach: duplicating rules per color scheme (avoid this) */
.card { background: white; color: #111827; }
@media (prefers-color-scheme: dark) {
.card { background: #1e1b2e; color: #f8fafc; } /* duplicated selector */
}
/* Better: override custom properties, keep the rule itself unchanged */
:root {
--surface: white;
--on-surface: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--surface: #1e1b2e;
--on-surface: #f8fafc;
}
}
.card { background: var(--surface); color: var(--on-surface); }
3. Die color-scheme Property und native Formularelemente
Neben der Media Query gibt es die CSS-Property color-scheme, die dem Browser mitteilt, welche Farbschemata eine Seite unterstützt. Mit :root { color-scheme: light dark; } signalisiert eine Seite, dass sie sowohl helles als auch dunkles Rendering beherrscht. Der Browser passt daraufhin automatisch native UI-Elemente an, die nicht direkt per CSS gestylt werden, etwa Scrollbars, Formularfelder, Checkboxen und Datumsauswahl-Widgets.
Ohne diese Property kommt es häufig zu einem inkonsistenten Bild: Der selbst gestaltete Seiteninhalt folgt prefers-color-scheme korrekt und wirkt dunkel, während native Scrollbars und Formularelemente weiterhin hell und grell dazwischen erscheinen. Die color-scheme Property behebt genau dieses Problem, indem sie native Elemente in die Theme-Entscheidung einbezieht, ohne dass jedes einzelne Element manuell umgestaltet werden muss.
/* Tell the browser which color schemes the page supports */
:root {
color-scheme: light dark;
}
/* Native form elements, scrollbars and date pickers now
automatically follow prefers-color-scheme without extra CSS */
input, textarea, select {
background: var(--surface);
color: var(--on-surface);
}
4. Custom Properties automatisch je Farbschema umschalten
Die Kombination aus prefers-color-scheme und Custom Properties ist der Standardansatz für wartbare Themes. Statt zwei komplett getrennte Stylesheets zu pflegen, definiert man ein einziges Set semantischer Variablen, etwa --surface, --on-surface und --border, und überschreibt nur deren Werte innerhalb der prefers-color-scheme Media Query. Alle Komponenten referenzieren ausschließlich diese semantischen Variablen, nie direkte Farbwerte.
Dieser Ansatz skaliert gut, weil neue Komponenten automatisch themefähig sind, sobald sie die vorhandenen semantischen Variablen verwenden, ohne dass zusätzlicher Theme-Code geschrieben werden muss. Seit der neueren light-dark() Funktion lässt sich dieselbe Logik sogar noch kompakter direkt in der Variablendefinition ausdrücken, ohne die Media Query überhaupt explizit zu schreiben, wobei prefers-color-scheme und die color-scheme Property dafür weiterhin die Grundlage bilden.
5. Nutzer-Override trotz Betriebssystem-Einstellung
Viele Produkte wollen trotz prefers-color-scheme einen manuellen Theme-Toggle anbieten, damit Nutzer unabhängig von der Systemeinstellung wählen können. Das übliche Muster: Ein data-theme-Attribut am html-Element, das per JavaScript gesetzt und in localStorage gespeichert wird, überschreibt bei explizitem Nutzerwunsch die automatische prefers-color-scheme Erkennung. Fehlt das Attribut, greift weiterhin die Systemeinstellung als Standard.
Wichtig für ein sauberes Zusammenspiel: Die CSS-Selektoren für den manuellen Override müssen spezifischer sein als die reine Media Query, etwa [data-theme="dark"] als Attribut-Selektor, der die Media-Query-Regeln gezielt überschreibt. So bleibt prefers-color-scheme der sinnvolle Standardwert, während ein expliziter Nutzerwunsch stets Vorrang hat, ganz gleich, was das Betriebssystem meldet.
/* System preference as the default */
:root {
--surface: white;
--on-surface: #111827;
}
@media (prefers-color-scheme: dark) {
:root { --surface: #1e1b2e; --on-surface: #f8fafc; }
}
/* Explicit user override always wins, regardless of the OS setting */
:root[data-theme="light"] { --surface: white; --on-surface: #111827; }
:root[data-theme="dark"] { --surface: #1e1b2e; --on-surface: #f8fafc; }
// Persist and apply an explicit user override
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
}
// Without a saved value, prefers-color-scheme remains the source of truth
6. Testing in DevTools und Regressions-Erkennung
Chrome, Firefox und Safari erlauben im DevTools-Rendering-Tab, prefers-color-scheme unabhängig von der tatsächlichen Betriebssystemeinstellung zu emulieren. Das ist essenziell, weil ein Wechsel der echten Systemeinstellung während der Entwicklung umständlich ist und beide Zustände regelmäßig getestet werden müssen. Ein häufig übersehener Testfall: Elemente, die nur in einem der beiden Schemata sichtbar sind, etwa Icons mit fest codierter Farbe statt einer Custom Property, fallen bei diesem Test sofort auf.
Für automatisierte visuelle Regressionstests mit Playwright oder Puppeteer lässt sich prefers-color-scheme ebenfalls per Browser-Kontext-Option emulieren, sodass Screenshot-Vergleiche für beide Themes in der CI-Pipeline unabhängig voneinander laufen. Das deckt zuverlässig auf, wenn eine neue Komponente versehentlich eine hartkodierte Farbe statt einer themefähigen Variable verwendet.
7. Flash of wrong theme vermeiden bei SSR
Bei serverseitig gerenderten Seiten kann es zu einem kurzen Aufblitzen des falschen Themes kommen, wenn der Server das Theme nicht kennt und der Browser zunächst das Standard-Theme rendert, bevor JavaScript den gespeicherten Nutzer-Override anwendet. Dieses Problem ist unabhängig von prefers-color-scheme selbst, tritt aber häufig in Kombination mit einem Nutzer-Override auf, weil der Server den localStorage-Wert des Browsers nicht kennt.
Die gängige Lösung: Ein winziges Inline-Skript im <head>, das noch vor dem ersten Render der Seite ausgeführt wird und das data-theme-Attribut synchron setzt, bevor der Browser überhaupt mit dem Rendering beginnt. Da prefers-color-scheme selbst rein deklarativ und ohne JavaScript funktioniert, betrifft dieses Problem ausschließlich Seiten mit zusätzlichem manuellem Override, nicht den reinen automatischen Systemmodus.
<!-- Inline script in <head>, runs before first paint -->
<script>
(function () {
var saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
}
})();
</script>
8. Kombination mit prefers-contrast und forced-colors
prefers-color-scheme deckt nur die Achse Hell gegen Dunkel ab, nicht Kontrastanforderungen. Die verwandte Media Query prefers-contrast erkennt, ob der Nutzer erhöhten oder reduzierten Kontrast bevorzugt, und forced-colors erkennt den Windows High Contrast Mode, in dem der Browser eigene, vom System vorgegebene Farben erzwingt. Alle drei Media Queries lassen sich unabhängig voneinander kombinieren, weil sie unterschiedliche Nutzerpräferenzen abbilden.
Ein vollständig barrierefreies Farbsystem berücksichtigt prefers-color-scheme für die grundlegende Hell-Dunkel-Wahl, prefers-contrast für zusätzliche Kontraststufen innerhalb jedes Schemas, und respektiert forced-colors, indem es dort keine eigenen Hintergrundfarben erzwingt, sondern dem Betriebssystem die Kontrolle überlässt. Projekte, die nur prefers-color-scheme implementieren und die anderen beiden ignorieren, decken nur einen Teil der tatsächlichen Nutzerpräferenzen ab.
9. Implementierungsansätze im direkten Vergleich
Die folgende Tabelle vergleicht drei gängige Implementierungsansätze für Dark Mode.
| Kriterium | Nur Media Query | Media Query + Override | Nur JS-Toggle |
|---|---|---|---|
| Folgt Systemeinstellung | Ja, automatisch | Ja, als Standard | Nein |
| Manuelle Nutzerwahl | Nicht möglich | Ja, mit Vorrang | Ja |
| JavaScript nötig | Nein | Nur fürs Speichern | Ja, zwingend |
| Flash-Risiko bei SSR | Keines | Vermeidbar mit Inline-Skript | Hoch ohne Inline-Skript |
| Wartungsaufwand | Gering | Mittel | Hoch |
Für die meisten Projekte ist die Kombination aus prefers-color-scheme als Standardwert und einem optionalen, per Attribut überschreibenden Nutzer-Toggle der beste Kompromiss. Reine JS-Toggles ohne Berücksichtigung der Systemeinstellung ignorieren eine bereits getroffene Nutzerentscheidung und sollten nur in Ausnahmefällen die alleinige Lösung sein.
Mironsoft
Dark Mode Implementierung, Theme-Architektur und Barrierefreiheit
Dark Mode ohne Flash of wrong theme?
Wir implementieren prefers-color-scheme mit sauberem Nutzer-Override, ohne sichtbares Aufblitzen beim ersten Laden, und stellen sicher, dass native Formularelemente, Kontrast-Präferenzen und Windows High Contrast Mode korrekt zusammenspielen.
Theme-Architektur
Semantische Custom Properties statt doppelter Farbregeln pro Schema
SSR-Fix
Inline-Skript gegen Flash of wrong theme bei serverseitigem Rendering
Barrierefreiheit
Kombination mit prefers-contrast und forced-colors korrekt umgesetzt
10. Zusammenfassung
prefers-color-scheme ist der Ausgangspunkt für jeden modernen Dark Mode, aber erst im Zusammenspiel mit weiteren Techniken entsteht ein wirklich sauberes Ergebnis. Die color-scheme Property bindet native UI-Elemente in die Theme-Entscheidung ein, semantische Custom Properties reduzieren Wartungsaufwand, und ein Attribut-basierter Nutzer-Override erlaubt eine bewusste Abweichung von der Systemeinstellung, ohne die Media Query als sinnvollen Standard zu verlieren.
Bei serverseitig gerenderten Anwendungen verhindert ein winziges Inline-Skript im Head das Aufblitzen des falschen Themes, während die Kombination mit prefers-contrast und forced-colors dafür sorgt, dass auch Nutzer mit besonderen Kontrastanforderungen vollständig berücksichtigt werden. Wer alle diese Bausteine zusammenführt, erhält ein Dark Mode System, das sich für Nutzer wie eine native, nahtlose Betriebssystemfunktion anfühlt.
prefers-color-scheme im Detail — Das Wichtigste auf einen Blick
Grundmuster
@media (prefers-color-scheme: dark) überschreibt Custom Properties, nie einzelne CSS-Regeln direkt duplizieren.
color-scheme Property
color-scheme: light dark; passt native Scrollbars und Formularelemente automatisch an.
Flash of wrong theme
Inline-Skript im Head setzt das Attribut synchron, noch vor dem ersten Render der Seite.
Vollständige Barrierefreiheit
prefers-contrast und forced-colors zusätzlich zu prefers-color-scheme berücksichtigen.