</>
tw
Tailwind CSS · Dark Mode · @variant · CSS Custom Properties
Tailwind CSS Dark Mode Strategie
class, media und @variant in v4 richtig einsetzen

Dark Mode in Tailwind CSS ist mehr als ein dark:-Präfix vor jeder Klasse. Eine durchdachte Dark-Mode-Strategie kombiniert Systemsynchronisation, Nutzerpräferenz-Persistenz, CSS Custom Properties und einen flicker-freien Toggle – von v3 bis zu den neuen @variant-Möglichkeiten in v4.

13 Min. Lesezeit dark: Variant · @variant · prefers-color-scheme · localStorage · @theme Tailwind CSS v3 & v4

1. Dark Mode in Tailwind CSS: die zwei Grundstrategien

Tailwind CSS Dark Mode lässt sich auf zwei grundlegende Weisen implementieren: über das CSS Media Feature prefers-color-scheme: dark, das die Systemeinstellung des Nutzers abfragt, oder über eine CSS-Klasse auf dem html- oder body-Element, die manuell gesetzt und per JavaScript gesteuert wird. Beide Strategien nutzen denselben Tailwind-Syntax im HTML – das dark:-Präfix vor Utility-Klassen. Was sich unterscheidet, ist die Mechanik, die bestimmt, wann diese Dark-Mode-Klassen aktiv sind.

Der systembasierte Ansatz mit prefers-color-scheme ist der einfachere der beiden: Man konfiguriert Tailwind entsprechend, und der Browser aktiviert automatisch alle dark:-Klassen, wenn das Betriebssystem auf Dark Mode gestellt ist. Das erfordert kein JavaScript und ist der zugänglichste Ansatz. Der Nachteil: Der Nutzer hat keine Kontrolle über die Website-spezifische Einstellung – wer sein System auf Dark Mode hat, bekommt immer Dark Mode, auch wenn er für diese spezifische Website lieber Light Mode hätte. Für die meisten professionellen Webanwendungen ist der Hybridansatz, der beide Strategien kombiniert, die richtige Wahl.

Die Tailwind CSS Dark Mode Strategie für ein Projekt hängt von mehreren Faktoren ab: Wie wichtig ist Nutzerkontrolle? Ist eine SSR-Implementierung nötig (wo Cookie-basiertes Dark Mode nötig ist)? Wie groß ist das Entwicklungsteam und wie gut sind CSS Custom Properties in der Codebasis etabliert? Die Antworten auf diese Fragen bestimmen, welche der im Folgenden beschriebenen Strategien die richtige ist.

2. Class-basierter Dark Mode: Implementierung und Toggle

Der class-basierte Tailwind CSS Dark Mode wird durch eine CSS-Klasse auf dem html-Element aktiviert. In Tailwind CSS v3 konfiguriert man das über darkMode: 'class' in tailwind.config.js. Damit aktiviert Tailwind alle dark:-Präfix-Klassen, sobald das html-Element die Klasse dark trägt. Dieser Ansatz gibt JavaScript vollständige Kontrolle darüber, wann Dark Mode aktiv ist – man kann auf Nutzerpräferenzen, gespeicherte Einstellungen und System-Events reagieren.

Ein vollständiger Dark-Mode-Toggle in Vanilla JavaScript für den class-basierten Tailwind CSS Dark Mode besteht aus drei Teilen: dem initialen Laden der gespeicherten Präferenz (aus localStorage), dem Toggle-Button-Handler, der die dark-Klasse setzt und entfernt, und dem System-Event-Listener, der auf Änderungen von prefers-color-scheme reagiert, wenn keine explizite Nutzerpräferenz gespeichert ist. Diese drei Teile zusammen ergeben eine robuste Dark-Mode-Implementierung, die sowohl gespeicherte Präferenzen als auch Systemänderungen korrekt handhabt.


// dark-mode.js — Complete Tailwind CSS Dark Mode implementation
// Supports: localStorage persistence, system sync, no-flicker

(function () {
  'use strict';

  const STORAGE_KEY = 'color-scheme';
  const html = document.documentElement;

  // --- Initial setup (runs before paint to avoid flicker) ---
  function getStoredPreference() {
    try { return localStorage.getItem(STORAGE_KEY); } catch { return null; }
  }

  function getSystemPreference() {
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }

  function applyColorScheme(scheme) {
    if (scheme === 'dark') {
      html.classList.add('dark');
    } else {
      html.classList.remove('dark');
    }
    html.style.colorScheme = scheme;  // Tells browser to style native UI dark
  }

  // Apply preference immediately (before DOM paint = no FOUC)
  const stored = getStoredPreference();
  applyColorScheme(stored ?? getSystemPreference());

  // --- Toggle function (called by the UI button) ---
  window.toggleDarkMode = function () {
    const isDark = html.classList.contains('dark');
    const next   = isDark ? 'light' : 'dark';
    applyColorScheme(next);
    try { localStorage.setItem(STORAGE_KEY, next); } catch {}
  };

  // --- Sync with system changes (when no stored preference) ---
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    if (!getStoredPreference()) {
      applyColorScheme(e.matches ? 'dark' : 'light');
    }
  });
})();

Das Script muss im <head> synchron geladen werden – nicht mit defer oder async. Das ist der entscheidende Punkt für einen flicker-freien Tailwind Dark Mode: Das Script läuft, bevor der Browser den ersten Frame rendert, und setzt die dark-Klasse bereits vor dem Paint. Wenn das Script asynchron geladen würde, würde die Seite kurzzeitig im falschen Modus erscheinen, bevor JavaScript die korrekte Einstellung setzt – der sogenannte Flash of Unstyled Content (FOUC) für Dark Mode.

3. prefers-color-scheme: systembasierter Dark Mode

Der systembasierte Tailwind CSS Dark Mode über prefers-color-scheme ist in Tailwind CSS v4 der Standardmodus. Das dark:-Variant reagiert ohne weitere Konfiguration auf die Systemeinstellung. In v3 erforderte dieser Modus darkMode: 'media' in tailwind.config.js (oder war der Standard, wenn kein darkMode konfiguriert war). In v4 ist prefers-color-scheme automatisch der Standard, weil keine explizite Dark-Mode-Konfiguration mehr nötig ist.

Der rein systembasierte Ansatz ist ideal für Projekte, bei denen die Zielgruppe technisch versiert ist und erwartet, dass Websites ihre Systemeinstellungen respektieren. Entwickler-Tools, API-Dokumentationen und technische Blogs fallen häufig in diese Kategorie. Für E-Commerce-Anwendungen, Marketing-Seiten oder Webanwendungen mit breiter Zielgruppe ist der Hybridansatz mit Toggle die bessere Wahl, weil nicht alle Nutzer ihre System-Dark-Mode-Einstellung kennen oder bedienen können.

4. Hybridansatz: System + Nutzerpräferenz kombinieren

Der Hybridansatz kombiniert das Beste aus beiden Tailwind CSS Dark Mode Strategien: Er startet mit der Systemeinstellung als Standardwert und erlaubt dem Nutzer, diese Einstellung für die Website zu überschreiben und zu persistieren. Das Resultat ist eine Dark-Mode-Implementierung, die für alle Nutzer korrekt startet – diejenigen mit System-Dark-Mode bekommen Dark Mode, diejenigen mit System-Light-Mode bekommen Light Mode – und gleichzeitig Nutzerkontrolle bietet.

Technisch unterscheidet der Hybridansatz zwischen drei Zuständen: auto (folgt der Systemeinstellung), dark (explizit Dark Mode unabhängig vom System), und light (explizit Light Mode unabhängig vom System). Im localStorage speichert man dark oder light für explizite Präferenzen, und nichts (oder auto), wenn der Nutzer die Systemeinstellung nutzen soll. Ein Dreifach-Toggle – oder ein Auswahlfeld mit drei Optionen – gibt dem Nutzer volle Kontrolle. Dieser Ansatz folgt dem Verhalten von modernen Betriebssystemen und Anwendungen wie VS Code oder GitHub.

5. Flicker-freier Dark Mode ohne FOUC

Der Flash of Unstyled Content (FOUC) bei Tailwind CSS Dark Mode entsteht, wenn das Dark-Mode-Script nach dem ersten Render-Zyklus des Browsers ausgeführt wird. Der Browser zeigt kurz die Light-Mode-Version, bevor JavaScript die dark-Klasse setzt und der Browser neu rendert. Dieser Flicker ist besonders störend bei Nutzern mit System-Dark-Mode oder gespeicherter Dark-Mode-Präferenz, die erwarten, dass die Seite sofort im richtigen Modus erscheint.

Die Lösung für flicker-freien Tailwind Dark Mode ist ein minimal gehaltenes, synchrones Inline-Script im <head> der HTML-Seite. Dieses Script liest die gespeicherte Präferenz und setzt die dark-Klasse vor dem ersten Paint. Es sollte so klein wie möglich sein – unter 200 Bytes sind ideal – weil synchrone Scripts den HTML-Parser blockieren. Das eigentliche Dark-Mode-Management-Script kann dann asynchron oder defer-geladen werden. Wichtig: Das Inline-Script setzt nur die dark-Klasse und die color-scheme-CSS-Property; alle weiteren Funktionen kommen im defer-geladenen Script.


<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">

  <!-- Anti-FOUC script: runs synchronously before first paint -->
  <!-- Keep this script minimal — it blocks HTML parsing -->
  <script>
    (function(){
      var s = localStorage.getItem('color-scheme');
      var p = window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (s === 'dark' || (!s && p)) {
        document.documentElement.classList.add('dark');
        document.documentElement.style.colorScheme = 'dark';
      }
    })();
  </script>

  <!-- Full dark mode logic deferred — does not block parsing -->
  <script defer src="/js/dark-mode.js"></script>

  <!-- Tailwind CSS with dark: variant ready from first paint -->
  <link rel="stylesheet" href="/css/app.css">
</head>
<body class="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">

  <!-- Dark mode toggle button -->
  <button
    onclick="toggleDarkMode()"
    class="p-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700"
    aria-label="Dark Mode umschalten">
    <!-- Sun icon (visible in dark mode) -->
    <svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
      <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm..."/>
    </svg>
    <!-- Moon icon (visible in light mode) -->
    <svg class="w-5 h-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
      <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
    </svg>
  </button>

</body>
</html>

6. @variant dark in Tailwind CSS v4

In Tailwind CSS v4 wird die Dark-Mode-Konfiguration von tailwind.config.js in CSS verlagert. Der darkMode-Schlüssel aus v3 entfällt komplett. Stattdessen definiert man das dark-Variant entweder implizit (der Standard reagiert auf prefers-color-scheme) oder explizit über eine @variant-Deklaration in der CSS-Eingabedatei. Für class-basierten Tailwind CSS Dark Mode in v4 schreibt man: @variant dark (&:where(.dark, .dark *));

Diese @variant-Deklaration überschreibt das Standard-Media-Query-Verhalten und bindet das dark:-Variant stattdessen an den Selektor :where(.dark, .dark *). Das bedeutet: Alle dark:-Klassen werden aktiv, wenn sich das Element selbst oder eines seiner Vorfahren in einem Element mit der Klasse dark befindet. Der :where()-Selektor hat keine Spezifität und verhindert damit unerwartete Spezifitätskonflikte. Für Projekte, die server-seitiges Rendering nutzen und den Dark Mode per Cookie steuern, lässt sich das Variant entsprechend anpassen: @variant dark (&:where([data-theme=dark], [data-theme=dark] *));

7. Dark Mode mit CSS Custom Properties und @theme

Die fortgeschrittenste Tailwind CSS Dark Mode Strategie kombiniert das dark:-Variant mit einem semantischen Design-Token-System aus CSS Custom Properties. Statt für jedes Element explizite dark:-Klassen zu definieren, definiert man semantische Tokens – wie --color-background, --color-foreground, --color-surface – und überschreibt diese Tokens im Dark Mode. Das bedeutet: Ein Element mit bg-background braucht keine dark:bg-slate-900-Klasse, weil der --color-background-Token im Dark Mode automatisch auf den dunklen Wert umschaltet.

In Tailwind CSS v4 mit @theme lässt sich dieses Muster besonders elegant implementieren. Man definiert die Light-Mode-Tokens in @theme, und die Dark-Mode-Overrides in einem @variant dark-Block oder einem @media (prefers-color-scheme: dark)-Block. Der Vorteil: Der HTML-Code wird erheblich sauberer – statt class="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-700" genügt class="bg-background text-foreground border-default". Das Dark Mode Verhalten ist vollständig in den CSS-Tokens gekapselt.


/* Tailwind CSS v4 — Dark Mode with semantic @theme tokens */
@import "tailwindcss";

/* Class-based dark mode variant */
@variant dark (&:where(.dark, .dark *));

@theme {
  /* Light mode tokens (defaults) */
  --color-background:    #ffffff;
  --color-surface:       #f8fafc;
  --color-surface-alt:   #f1f5f9;
  --color-foreground:    #0f172a;
  --color-foreground-muted: #64748b;
  --color-border:        #e2e8f0;
  --color-interactive:   #2563eb;
}

/* Dark mode token overrides */
@variant dark {
  @theme {
    --color-background:    #0f172a;
    --color-surface:       #1e293b;
    --color-surface-alt:   #334155;
    --color-foreground:    #f8fafc;
    --color-foreground-muted: #94a3b8;
    --color-border:        #334155;
    --color-interactive:   #60a5fa;
  }
}

/* Base styles using semantic tokens — automatically dark-mode-aware */
@layer base {
  body {
    background-color: var(--color-background);
    color: var(--color-foreground);
  }

  hr, [class*="border"] {
    border-color: var(--color-border);
  }
}

/* Result: no dark: prefixes needed in HTML for most elements */
/* <div class="bg-background text-foreground border-border"> — works in both modes */

8. Dark-Mode-Strategien im Vergleich

Der direkte Vergleich der verschiedenen Tailwind CSS Dark Mode Strategien zeigt, welcher Ansatz für welche Projektanforderungen am besten geeignet ist.

Strategie Nutzerkontrolle SSR-kompatibel HTML-Menge Beste für
prefers-color-scheme Nur System Ja Mittel Dev-Tools, Blogs
class-basiert (Toggle) Vollständig Mit Cookie Mittel Apps, E-Commerce
Hybrid (System + Toggle) System + Override Mit Cookie Mittel Alle öffentlichen Websites
Semantic Tokens + @theme Vollständig Ja Gering Design-System, große Teams

Für die meisten modernen Webprojekte ist die Kombination aus class-basiertem Tailwind CSS Dark Mode mit semantischen Tokens die optimale Strategie. Sie bietet Nutzerkontrolle, sauberes HTML und eine wartbare CSS-Architektur. Das FOUC-Problem wird durch das synchrone Inline-Script gelöst, die SSR-Kompatibilität durch Cookies für den initialen Server-Render. In Tailwind CSS v4 ist diese Strategie mit @variant dark und Token-Overrides in @theme besonders elegant implementierbar.

9. Best Practices und häufige Fehler

Der häufigste Fehler bei der Tailwind CSS Dark Mode-Implementierung ist das Vergessen des color-scheme-CSS-Property. Dieses Property teilt dem Browser mit, dass die Seite einen Dark Mode hat, und bewirkt, dass native Browser-UI-Elemente – Scrollbars, Formulare, Datei-Picker – ebenfalls im Dark Mode erscheinen. Ohne html { color-scheme: dark; } hat man dunkle Hintergründe durch Tailwind-Klassen, aber helle Scrollbars und helle Formular-Elemente – ein inkonsistentes Ergebnis. Das color-scheme-Property wird programmatisch im Dark-Mode-Script gesetzt: document.documentElement.style.colorScheme = 'dark'.

Ein zweiter häufiger Fehler betrifft die Bildbehandlung im Tailwind CSS Dark Mode. Helle Bilder auf dunklem Hintergrund wirken oft zu hart. Die CSS-Eigenschaft img { filter: brightness(0.9) contrast(1.05); } unter einem Dark-Mode-Selector gibt Bildern eine weichere Erscheinung im Dark Mode. Als Tailwind CSS Custom Utility lässt sich das elegant lösen: @utility dark-img-adjust mit den entsprechenden Filter-Werten und Anwendung der Klasse über den dark:-Variant. Für Logos und Icons empfiehlt sich die Verwendung von SVG mit currentColor, die automatisch mit der Textfarbe umschalten.

10. Zusammenfassung

Eine durchdachte Tailwind CSS Dark Mode Strategie ist mehr als das Hinzufügen von dark:-Präfixen zu Utility-Klassen. Sie beginnt mit der richtigen Architekte-Entscheidung – systembasiert, class-basiert oder hybrid – und umfasst ein semantisches Token-System für wartbares HTML, ein flicker-freies Initialisierungs-Script für das beste Nutzererlebnis und die korrekte Behandlung von nativen Browser-Elementen über color-scheme.

Tailwind CSS v4 vereinfacht die Dark-Mode-Konfiguration erheblich: Die @variant dark-Direktive ersetzt darkMode: 'class' aus der tailwind.config.js, und Token-Overrides in @theme innerhalb von @variant dark-Blöcken machen semantische Dark-Mode-Systeme ohne doppelte Klassen im HTML möglich. Das Ergebnis ist eine Dark-Mode-Implementierung, die wartbar, zugänglich und für alle Nutzer – unabhängig von ihren System-Einstellungen – korrekt funktioniert.

Tailwind CSS Dark Mode Strategie — Das Wichtigste auf einen Blick

Kein FOUC

Minimales Inline-Script im head – synchron ausgeführt, bevor der Browser rendert. dark-Klasse vor dem ersten Paint setzen.

@variant dark in v4

@variant dark (&:where(.dark, .dark *)) ersetzt darkMode: 'class' in tailwind.config.js vollständig.

Semantische Tokens

@theme-Tokens im @variant dark Block überschreiben – sauberes HTML ohne doppelte dark:-Klassen für jedes Element.

color-scheme Property

html { color-scheme: dark } für native Browser-Elemente (Scrollbars, Formulare). Ohne dieses Property sind native UI-Elemente immer hell.