Tailwind Dark Mode: class vs. data-theme im Vergleich
Auf dem Bildschirm blinkt dark:bg-gray-900 – und die Frage bleibt: Tailwind Dark Mode mit class oder data-theme?
Beide Varianten haben Zeit gekostet. In der Doku findet man meist nur Fragmente; zusammengesetzt fehlt oft das Gesamtbild. Irgendwann habe ich offizielle Docs, GitHub-Diskussionen und den Quellcode beliebter UI-Bibliotheken durchgearbeitet – und die Unterschiede klarer gesehen. Dieser Artikel fasst Stolpersteine und Abwägungen zusammen.
Drei Dark-Mode-Strategien in Tailwind
Wichtig vorweg: Tailwind bietet drei Strategien – nicht nur zwei.
Media-Strategie: Systemeinstellung folgen
Media ist der Standard – viele kennen das gar nicht. Über prefers-color-scheme reagiert das Layout auf die Systempräferenz.
<!-- Keine Extra-Konfiguration – folgt dem System -->
<div class="bg-white dark:bg-gray-900">
Inhalt wechselt automatisch mit der Systemeinstellung
</div>
Vorteil: null Aufwand, oft passendes Erlebnis. Nachteil: keine manuelle Wahl – wer tagsüber Dunkelmodus will, ist schlechter bedient.
Class-Strategie: manuell steuern
Bei der Class-Strategie setzen Sie auf dem Elternelement (meist <html>) die Klasse .dark – dann greifen alle dark:*-Utilities.
<html class="dark">
<body class="bg-white dark:bg-gray-900">
Dunkelmodus aktiv
</body>
</html>
Das ist der verbreitetste Weg: viel Doku, viele Libraries.
Data-theme-Strategie: semantisches Attribut
Statt einer Klasse nutzen Sie data-theme="dark". Semantik ist klarer; Mehrfach-Themen sind einfacher.
<html data-theme="dark">
<body class="bg-white dark:bg-gray-900">
Dunkelmodus aktiv
</body>
</html>
data-theme="oled" oder data-theme="sepia" – beliebig erweiterbar. Praktisch, wenn mehr als Hell/Dunkel nötig ist.
Class-Strategie im Detail
Funktionsweise
Liegt .dark auf einem Vorfahren, werden dark:*-Styles aktiv.
Tailwind v3 – in der Config:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
Erzeugtes CSS etwa so:
.dark .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v4 – CSS-first mit @custom-variant:
/* global.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
:where() hält die Spezifität niedrig – wichtig für spätere Overrides.
JavaScript zum Umschalten
function getTheme() {
return localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
}
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
}
setTheme(getTheme());
Lesen aus localStorage, Fallback System, speichern und Klasse setzen – reicht für die meisten Fälle.
Flash beim Laden vermeiden
Kurz hell, dann dunkel: HTML rendert, bevor JS läuft. Lösung – synchrones Script im <head>:
<head>
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
</head>
Kein defer, kein async.
Vor- und Nachteile
Vorteile:
- Einfach, schnell einsteigen
- Viele Community-Beispiele und Framework-Anleitungen
- Gute Integration mit next-themes u. Ä.
- Etwas höhere Spezifität durch Klassen
Nachteile:
.darkist nicht selbsterklärend- Mehrere Themes = mehrere Klassen, unübersichtlicher
- Mit CSS-Variablen oft Extra-Anpassung
Data-theme-Strategie im Detail
Funktionsweise
Attribut-Selektoren statt Klassen. Tailwind v4:
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
Erzeugtes CSS:
[data-theme='dark'] .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v3 – Array-Konfiguration:
module.exports = {
darkMode: ['selector', '[data-theme="dark"]'],
}
Mit CSS-Variablen
Data-theme und CSS-Variablen passen gut zusammen:
:root {
--background: 0 0% 100%;
--foreground: 222 84% 5%;
}
[data-theme='dark'] {
--background: 222 84% 5%;
--foreground: 210 40% 98%;
}
[data-theme='oled'] {
--background: 0 0% 0%;
--foreground: 0 0% 100%;
}
In Tailwind referenzieren:
module.exports = {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
}
}
}
}
data-theme ändern – alle abhängigen Styles folgen, ohne dark: überall.
Praxis bei shadcn/ui
shadcn/ui nutzt standardmäßig data-theme plus Variablen, unterstützt aber auch .dark:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* ... */
}
.dark,
[data-theme='dark'] {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... */
}
}
Mit shadcn/ui können Sie beides triggern – eine Variante reicht.
Mehrere Themes
<html data-theme="oled">
</html>
<html data-theme="sepia">
</html>
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
So flexibel ist die Class-Strategie nur mit mehr Aufwand.
Vor- und Nachteile
Vorteile:
- Klare Semantik (
data-theme="dark") - Mehrfach-Themen natürlich
- Starke Kombination mit CSS-Variablen
- shadcn/ui, daisyUI u. Ä. kompatibel
Nachteile:
- In v3 Selector selbst konfigurieren
- Manche Libraries brauchen Anpassung
- Etwas weniger Doku als class – holt aber auf
Vergleichsmatrix
| Dimension | Class | Data-theme |
|---|---|---|
| Implementierung | Niedrig | Mittel |
| Semantik | Mittel (.dark) | Hoch (data-theme) |
| Mehrfach-Themen | Schwer | Einfach |
| Community | Hoch | Mittel |
| CSS-Variablen | Extra-Aufwand | Natürlich |
| Tailwind v3 | darkMode: 'class' | darkMode: ['selector', '...'] |
| Tailwind v4 | @custom-variant | @custom-variant |
| Libraries | Kompatibilität prüfen | shadcn/ui u. Ä. |
| Spezifität | Etwas höher (Klasse) | Attribut, vergleichbar |
Wann Class?
- Nur Hell/Dunkel
- Next.js + next-themes
- Team kennt v3-Config gut
- Viele Community-Referenzen gewünscht
Wann Data-theme?
- OLED, Sepia, weitere Themes
- shadcn/ui oder ähnlich
- Tiefe Integration mit CSS-Variablen
- Hohe Anforderung an Semantik
Framework-Integration
Astro
Integration ist unkompliziert – View Transitions sind die typische Falle.
Basis:
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
plugins: [tailwindcss()]
}
});
Script im Head (BaseLayout.astro):
<script is:inline>
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
// document.documentElement.dataset.theme = 'dark';
}
</script>
Nach View Transition:
<script>
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
</script>
Ohne astro:after-swap verliert sich der Modus oft – leicht zu übersehen.
Next.js + next-themes
next-themes kapselt Persistenz, Systemerkennung und Hydration.
npm install next-themes
import { ThemeProvider } from 'next-themes';
export function AppThemeProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem={true}
disableTransitionOnChange
>
{children}
</ThemeProvider>
);
}
Data-theme: attribute="data-theme".
export default function RootLayout({ children }) {
return (
<html lang="de">
<body>
<AppThemeProvider>{children}</AppThemeProvider>
</body>
</html>
);
}
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
);
}
Tailwind v4
@custom-variant
Varianten direkt in CSS:
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
Kein JS-Rebuild nur für Dark-Mode-Config.
@theme und Variablen
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
@theme {
--color-primary: oklch(0.65 0.2 150);
--color-muted: oklch(0.9 0.02 200);
}
[data-theme='dark'] {
--color-primary: oklch(0.7 0.15 180);
--color-muted: oklch(0.3 0.02 200);
}
<button class="bg-primary text-white">Button</button>
Kein dark:bg-primary-dark nötig.
Hell / Dunkel / System
function setTheme(theme) {
if (theme === 'system') {
localStorage.removeItem('theme');
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
} else {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
}
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
}
});
Best Practices
Empfehlung
- Einfaches Projekt: class + kleines Script
- shadcn/ui: data-theme + CSS-Variablen
- Mehrere Themes: data-theme
- Next.js: next-themes,
attributenach Bedarf - Astro: View Transitions nicht vergessen
Anti-Flash (vollständig)
<head>
<script is:inline>
(function() {
const theme = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (theme === 'dark' || (!theme && systemDark)) {
document.documentElement.classList.add('dark');
// document.documentElement.dataset.theme = 'dark';
}
})();
</script>
</head>
SSR und Hydration
Bei eigenem React-Code Hydration-Mismatch vermeiden – next-themes löst das. Sonst z. B.:
import { useEffect, useState } from 'react';
function useTheme() {
const [theme, setTheme] = useState('light');
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light');
}, []);
return theme;
}
Semantische Variablennamen
/* Empfohlen */
:root {
--background: ...;
--foreground: ...;
--primary: ...;
}
/* Nicht empfohlen */
:root {
--white: ...;
--black: ...;
}
Fazit
Class ist ausgereift und für die meisten Projekte mit nur Hell/Dunkel die sichere Wahl. Data-theme ist semantisch klarer und besser für Mehrfach-Themen und CSS-Variablen – besonders mit shadcn/ui.
In Tailwind v4 konfigurieren beide Wege mit @custom-variant schlank. Entscheidend sind Anforderungen und Stack. Und: Astro View Transitions sowie Next.js-Hydration sauber lösen – sonst leidet die UX trotz guter Strategie.
Referenzen
FAQ
Was unterscheidet @custom-variant in Tailwind v4 von der v3-Konfiguration?
Kann man class und data-theme gleichzeitig nutzen?
Zu viele dark:-Modifier – was tun?
Vorgehen:
1. Variablen in globals.css mit @theme definieren
2. Werte unter verschiedenen [data-theme] überschreiben
3. Variablen in tailwind.config.js referenzieren
Dann passt sich bg-primary automatisch an.
Dark-Mode-Status in Astro-Projekten geht verloren – warum?
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
Diesen Schritt übersehen viele.
Weißer Flash beim Laden – wie vermeiden?
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
Wichtig: kein defer oder async.
6 Min. Lesezeit · Veröffentlicht am: 28. März 2026 · Aktualisiert am: 13. Juli 2026
Tailwind & shadcn/ui in der Praxis
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Tailwind Responsive Layout in der Praxis: Container Queries und Breakpoint-Strategie
Container Queries und Breakpoint-Strategien in Tailwind CSS – vom Viewport zum Container: responsives Design auf Komponentenebene meistern.
Teil 6 von 14
Nächster
shadcn/ui Kompositionsmuster: Best Practices für mehrere Komponenten
Lernen Sie die Best Practices für shadcn/ui-Kompositionsmuster – Dialog+Form, DataTable+DropdownMenu und mehr. Mit Context-Muster, State-Management und Performance-Optimierung.
Teil 8 von 14
Ähnliche Beiträge
Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur

Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur
Tailwind CSS v4 – Neue Features: Performance, Konfiguration und Migrationsleitfaden


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen