Webdesign 13 Min. Lesezeit

Dark Mode mit semantischen Design-Tokens: [data-theme] und prefers-color-scheme

Dark Mode architektonisch korrekt implementieren: semantische Token-Ebenen, das [data-theme]-Toggle-Muster, prefers-color-scheme ohne Media-Query-Kopplung und CSS color-mix().

Dark Mode ist in vielen Projekten ein nachträglicher Gedanke — man fügt einen dark:-Prefix in Tailwind hinzu und hofft, dass es reicht. Das Ergebnis ist meistens inkonsistent: manche Farben stimmen, andere nicht, und der nächste Entwickler weiß nicht, was das Token-System ist. Dieser Artikel zeigt eine architektonisch saubere Herangehensweise.

Das Problem mit primitiven Tokens

Ein primitiver Token ist eine konkrete Farbe: --color-blue-500: #0072BB. Er beschreibt, was der Wert ist, nicht wozu er dient.

/* Primitive Tokens — beschreiben "Was" */
:root {
  --color-blue-500: #0072BB;
  --color-blue-700: #003d74;
  --color-gray-50: #f9fafb;
  --color-gray-900: #111827;
}

/* Komponente nutzt primitive Tokens direkt */
.button {
  background-color: var(--color-blue-500);
  color: var(--color-gray-50);
}

/* Dark Mode: alle Primitiven überschreiben — nicht wartbar */
@media (prefers-color-scheme: dark) {
  .button {
    background-color: var(--color-blue-700);
    color: var(--color-gray-900);
  }
}

Für jede Komponente muss man separat Dark Mode definieren. Bei 50+ Komponenten wird das unbeherrschbar.

Semantische Tokens: die Lösung

Semantische Tokens beschreiben wozu eine Farbe dient, nicht was sie ist. Sie sind eine Referenz-Ebene über den primitiven Tokens:

/* Primitive Tokens */
:root {
  --color-blue-500: #0072BB;
  --color-blue-400: #339ad8;
  --color-gray-50:  #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-800: #1f2937;
  --color-gray-900: #111827;
  --color-white:    #ffffff;
}

/* Semantische Tokens — Light Mode (Standard) */
:root {
  --color-surface-primary:   var(--color-white);
  --color-surface-secondary: var(--color-gray-50);
  --color-text-primary:      var(--color-gray-900);
  --color-text-secondary:    var(--color-gray-600);
  --color-border-default:    var(--color-gray-200);
  --color-interactive:       var(--color-blue-500);
  --color-interactive-hover: var(--color-blue-700);
}

Die Komponente nutzt nur semantische Tokens:

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

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

Das [data-theme]-Muster

Das [data-theme]-Attribut auf dem <html>-Element ist die sauberste Methode für einen manuell umschaltbaren Dark Mode:

/* semantics/dark.css */
[data-theme='dark'] {
  --color-surface-primary:   var(--color-gray-900);
  --color-surface-secondary: var(--color-gray-800);
  --color-text-primary:      var(--color-gray-50);
  --color-text-secondary:    var(--color-gray-400);
  --color-border-default:    var(--color-gray-700);
  --color-interactive:       var(--color-blue-400);
  --color-interactive-hover: var(--color-blue-300);
}

Im HTML:

<html data-theme="light">  <!-- oder data-theme="dark" -->

Der Toggle:

function toggleTheme() {
  const html = document.documentElement;
  const current = html.getAttribute('data-theme') ?? 'light';
  const next = current === 'light' ? 'dark' : 'light';
  html.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
}

System-Präferenz ohne Media-Query-Kopplung

Der häufige Fehler: man definiert Dark Mode in @media (prefers-color-scheme: dark) und via [data-theme] — das ergibt Konflikte. Eine klare Strategie:

// Beim Seitenstart (inline script im <head>, vor allem anderen)
function initTheme() {
  const saved = localStorage.getItem('theme');
  const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  const theme = saved ?? system;
  document.documentElement.setAttribute('data-theme', theme);
}

initTheme();

Das Script muss vor dem ersten Paint laufen — sonst gibt es ein Flash of Incorrect Theme (FOIT). In Astro:

---
// layouts/Layout.astro
---
<html>
  <head>
    <!-- Inline — MUSS vor allen anderen Scripts stehen -->
    <script is:inline>
      (function() {
        var saved = localStorage.getItem('theme');
        var system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        document.documentElement.setAttribute('data-theme', saved || system);
      })();
    </script>
    <!-- Rest des <head> -->
  </head>

is:inline verhindert, dass Astro das Script in den Bundle aufnimmt und verzögert ausführt.

Tailwind 4 Dark Mode mit [data-theme]

In Tailwind CSS 4 konfiguriert man die Dark-Mode-Variante:

/* global.css */
@import 'tailwindcss';

@custom-variant dark (&:where([data-theme='dark'] *));

Oder einfacher: Tailwind so konfigurieren, dass es data-theme als Selektor nutzt:

// tailwind.config.mjs
export default {
  darkMode: ['selector', '[data-theme="dark"]'],
};

Dann funktionieren alle dark:-Utilities von Tailwind automatisch mit dem [data-theme]-Toggle:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
  Inhalt
</div>

Farbanpassung mit color-mix()

CSS color-mix() erlaubt, Farben dynamisch zu variieren, ohne neue primitive Tokens zu definieren:

:root {
  --color-interactive: #0072BB;
  /* Hover: 15% dunkler */
  --color-interactive-hover: color-mix(in srgb, var(--color-interactive) 85%, black);
  /* Transparente Variante */
  --color-interactive-subtle: color-mix(in srgb, var(--color-interactive) 15%, transparent);
}

Im Dark Mode kann man color-mix() nutzen, um die gleiche Farbe heller statt dunkler zu machen:

[data-theme='dark'] {
  /* Im Dark Mode: heller statt dunkler für Hover */
  --color-interactive-hover: color-mix(in srgb, var(--color-interactive) 85%, white);
}

Transitions beim Theme-Wechsel

Ein abrupter Theme-Wechsel fühlt sich unpoliert an. Eine kurze Transition macht ihn angenehm:

/* Transition nur bei Theme-Wechsel, nicht beim initialen Laden */
.theme-transitioning,
.theme-transitioning * {
  transition:
    background-color 200ms ease,
    color 200ms ease,
    border-color 200ms ease !important;
}
function toggleTheme() {
  document.documentElement.classList.add('theme-transitioning');
  // Theme wechseln
  const next = current === 'light' ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
  // Transition-Klasse nach der Transition entfernen
  setTimeout(() => {
    document.documentElement.classList.remove('theme-transitioning');
  }, 300);
}

Die Transition-Klasse verhindert, dass die Übergänge beim Seiten-Laden aktiv sind — nur der manuelle Toggle soll animiert sein.

Token-Checkliste für ein vollständiges System

Ein robustes Dark Mode Token-System braucht mindestens:

/* Semantische Tokens — mindestens diese Rollen */
:root {
  /* Hintergründe */
  --color-surface-primary:    /* Haupthintergrund */;
  --color-surface-secondary:  /* Sekundäre Flächen, Cards */;
  --color-surface-tertiary:   /* Subtile Hintergründe */;
  --color-surface-inverse:    /* Invertierter Hintergrund für Badges etc. */;

  /* Text */
  --color-text-primary:       /* Haupttext */;
  --color-text-secondary:     /* Unterstützender Text */;
  --color-text-disabled:      /* Deaktivierter Text */;
  --color-text-inverse:       /* Text auf dunklem Hintergrund */;
  --color-text-link:          /* Link-Farbe */;

  /* Ränder */
  --color-border-default:     /* Standard-Trennlinie */;
  --color-border-strong:      /* Hervorgehobene Ränder */;
  --color-border-focus:       /* Fokus-Indikator */;

  /* Interaktiv */
  --color-interactive:        /* Primärer CTA */;
  --color-interactive-hover:  /* Hover-Zustand */;
  --color-interactive-pressed:/* Pressed-Zustand */;
  --color-interactive-subtle: /* Sekundäre Buttons */;
}

Weiterführende Artikel


Design System für Ihr Projekt? Wender Media entwirft skalierbare Token-Architekturen mit vollständiger Dark Mode Unterstützung — info@wendermedia.info.

Individuelle Beratung gewünscht?

Wender Media unterstützt Sie bei der praktischen Umsetzung — von der technischen Konzeption bis zum Launch. Schreiben Sie uns, wir antworten innerhalb von 24 Stunden.

Jetzt Beratung anfragen

Kostenlos & unverbindlich — info@wendermedia.info

Verwandte Artikel