Aktualisiert 11. Juli 2026

prefers-color-scheme: Richtig eingesetzt in CSS

prefers-color-scheme ist das CSS-Media-Feature, das meldet, welches Farbschema der Nutzer angefordert hat — light oder dark — so wie es im Betriebssystem oder Browser eingestellt ist. Richtig eingesetzt heißt: nur als Standard verwenden. Custom Properties definieren das helle Theme auf :root, die Media Query kippt diese Standardwerte, wenn das System Dark bevorzugt, und ein data-theme-Attribut, das ein manueller Umschalter setzt, überschreibt beides. Die Query allein ist noch keine Implementierung, denn die meisten echten Produkte brauchen auch den Umschalter — und ein Umschalter kann eine Media Query nicht aus CSS heraus überschreiben.

Das hier ist die Implementierungs-Referenz für die Umschalt-Mechanik aus unserem Dark-Mode-Leitfaden: die Query, das Override-Pattern und die vier Fallstricke, die die Support-Foren füllen.

Was liest die Media Query eigentlich aus?

Die erklärte Präferenz des Nutzers, weitergereicht aus der Umgebung: Die Erscheinungsbild-Einstellung des Betriebssystems (oder ein Override auf Browser-Ebene, wo es einen gibt) taucht in CSS als Media-Feature auf, genau wie eine Viewport-Breite. Die einfachste Form:

@media (prefers-color-scheme: dark) {
  /* Styles für Nutzer, die Dark angefordert haben */
}

Kein JavaScript ist im Spiel, und die Query ist live: Ändere die OS-Einstellung, und die passenden Styles greifen sofort. Die Werte des Features und Hinweise zum Support stehen auf MDNs Referenzseite. Wovon die Query nichts weiß, ist deine UI — sie meldet das System, nicht den Button in deinem Header.

Warum muss ein manueller Umschalter die Media Query überschreiben?

Weil jedes echte Produkt am Ende drei Zustände hat, nicht zwei: dem System folgen, erzwungen hell und erzwungen dunkel. Ein Nutzer, dessen OS auf Dark läuft, will deine App vielleicht trotzdem hell (lange Lesesitzungen sind ein häufiger Grund) — und umgekehrt. Die Media Query kann nur den ersten Zustand ausdrücken — deshalb legt das robuste Pattern ein Attribut obendrauf:

:root {
  color-scheme: light dark;
  --background:   oklch(0.94 0.008 262.9);
  --text-primary: oklch(0.28 0.02 262.9);
}

/* Standard: dem System folgen */
@media (prefers-color-scheme: dark) {
  :root {
    --background:   oklch(0.22 0.015 262.9);
    --text-primary: oklch(0.94 0.008 262.9);
  }
}

/* Erzwungene Zustände: das Attribut gewinnt über den Standard */
[data-theme="light"] {
  color-scheme: light;
  --background:   oklch(0.94 0.008 262.9);
  --text-primary: oklch(0.28 0.02 262.9);
}

[data-theme="dark"] {
  color-scheme: dark;
  --background:   oklch(0.22 0.015 262.9);
  --text-primary: oklch(0.94 0.008 262.9);
}

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

Lies es als eine Kaskade von Standardwerten: :root sagt hell, die Media Query sagt „außer das System bevorzugt Dark”, das Attribut sagt „außer der Nutzer hat uns etwas anderes gesagt”. Ohne Attribut folgt die Seite dem System; mit einem gewinnt die In-App-Wahl des Nutzers. Der JavaScript-Teil ist eine Zeile plus Speicherung:

const saved = localStorage.getItem("theme");   // "light" | "dark" | null
if (saved) document.documentElement.dataset.theme = saved;

null — der dritte Zustand — bedeutet „kein Attribut setzen und die Media Query entscheiden lassen”. Explizit "system" zu speichern und das Attribut zu entfernen, funktioniert genauso.

Das ist das komplette durchgearbeitete Beispiel: rund fünfundzwanzig Zeilen CSS und drei Zeilen JavaScript decken System-Folgen, beide erzwungenen Zustände und native Formularelemente ab. Die Werte selbst kommen paarweise — einer pro Theme, pro Rolle — und genau hier reicht die Token-Ebene dem Pattern ihre Daten.

Sieh dir in Scale Composer ein Rollen-Set an, das die Werte beider Themes hält — jede semantische Rolle mit ihrem hellen und dunklen Wert nebeneinander, bereit zum Einfügen in die beiden Blöcke oben.

Eine Liste semantischer Rollen in Scale Composer, die helle und dunkle Werte in parallelen Spalten für dieselben Rollennamen zeigt

Welche Fallstricke füllen die Support-Foren?

Formularelemente ignorieren dein Theme. Dein CSS färbt die Seite dunkel, aber Scrollbars, Checkboxen und Dropdowns bleiben hell — weil der Browser die zeichnet, und dem hat es niemand gesagt. Der Fix ist der Einzeiler, der schon im Pattern oben steht: color-scheme: light dark sagt dem Browser, welche Schemata die Seite unterstützt, und die Blöcke für die erzwungenen Zustände engen das ein, sodass native Elemente dem Override folgen, nicht nur der Query.

Das Aufblitzen des falschen Themes. Ein Nutzer mit Dark-Präferenz lädt die Seite, sieht ein weißes Aufblitzen, dann greift das Theme. Das passiert, wenn der gespeicherte Override von einem Skript gesetzt wird, das erst nach dem ersten Paint läuft. Der ehrliche Fix ist ein Inline-Skript im <head> — vor dem stylesheet-abhängigen Rendering, nicht deferred, nicht gebündelt — das localStorage liest und das Attribut setzt. Es blockiert das Rendering absichtlich; halte es auf diese wenigen Zeilen begrenzt.

Bilder und Embeds folgen nicht. Ein <picture>-Element kann Quellen pro Theme tauschen mit media="(prefers-color-scheme: dark)", aber das folgt dem System, nicht deinem data-theme-Override — eine bekannte Asymmetrie des Attribut-Patterns. Iframes lösen die Query in ihrem eigenen Kontext auf. Für Inhalte, die dem In-App-Umschalter folgen müssen, muss CSS (oder ein kleines Skript) das Tauschen übernehmen.

Der Umschalter animiert alles. Ein pauschales transition: all auf theme-betroffenen Elementen macht aus dem Umschalten eine langsame Überblendung jeder Farbe auf dem Bildschirm. Beschränke Transitions auf die Eigenschaften, die sie brauchen, oder deaktiviere Transitions kurz während des Umschaltens.

Macht die Query das Dark Theme gut?

Nein — sie beantwortet nur, welches Theme gezeigt wird. Ob das Dark Theme das Umschalten wert ist, entscheidet die Palette hinter den Custom Properties: Ein dunkles Set, abgeleitet mit einer eigenen Helligkeitskurve und einem Chroma-Boost, liest sich wie ein gestaltetes Theme, während invertierte helle Werte sich wie das Negativ eines solchen lesen. Die Query ist das Routing; die Werte sind das Produkt. Diese Werte abzuleiten ist das Thema des Rests dieses Hubs.

Prüfe das Pattern gegen deine eigenen Werte

Die beiden Blöcke im Pattern sind nur so gut wie die Paare, mit denen du sie füllst. Lade eine Palette und prüfe den Anker jeder Rolle unter beiden Themes — dieselben Rollennamen, die pro Theme aufgelöst werden, Kontrast-Untergrenzen auf jeder Seite neu geprüft, sodass die Werte, die du in die Media Query und den Attribut-Block einfügst, bereits verifizierte Paare sind.

Weiterlesen

  • Semantische Tokens für den Theme-Wechsel

    Dark mode design tokens erklärt: semantische Rollen, die einen Wert pro Theme halten, warum das Dark-Mapping kein Spiegelbild ist, und das CSS, das die Themes umschaltet.

  • Dark Mode ist keine Invertierung

    Dark-Mode-Design beginnt dort, wo Invertierung scheitert: umgekehrte Rollenlogik, kaputte Elevation, ausgelaugte Farben. Was es bedeutet, eine dunkle Palette aus denselben Ausgangsfarben abzuleiten.