Tailwind v4 Dark Mode: Klassen-Toggle, Tokens, next-themes

Du baust einen Mond-Button ein, er setzt class="dark" auf <html>, und nichts passiert. Oder die Seite wird dunkel, sobald macOS dunkel wird, egal was der Button sagt. Beim Tailwind v4 Dark Mode entscheidet ein einziger Schalter über all das, und der ist aus tailwind.config.js in dein CSS gewandert. Wie du den Tailwind Dark Mode einrichten kannst, zeigt dir dieses Tutorial Schritt für Schritt: den Schalter selbst, wie next-themes in Next.js daran andockt, die Checkliste für den Fall, dass es trotzdem nicht klappt, und wie du das dunkle Theme gestaltest, statt es einfach zu invertieren. Dafür nutzt du semantische Tokens und einen Toggle, der schon vom Server richtig gerendert wird.
Alles hier haben wir am 13. Oktober 2026 mit Tailwind CSS 4.3.3, next-themes 0.4.6, Next.js 16.3 und React 19.3 geprüft. Jeder Block mit kompiliertem CSS ist echte Ausgabe von Tailwind 4.3.3.
Warum folgt Tailwind v4 Dark Mode dem Betriebssystem statt meinem Toggle?
Weil das der Default ist. Ohne Konfiguration kompiliert Tailwind v4 dark:bg-black zu einer Media Query:
Eine Klasse auf <html> kann eine Media Query nicht ändern, dein Toggle hat also keinen Angriffspunkt. Die Tailwind-Doku zum Dark Mode (opens in a new tab) sagt es in einem Satz: Standardmäßig richtet sich die Variante nach dem CSS-Media-Feature prefers-color-scheme. Willst du selbst steuern, definierst du die Variante in dem Stylesheet neu, das Tailwind importiert:
Jetzt kompiliert dieselbe Utility zu einem Selektor:
Kommst du von v3, ersetzt genau diese Zeile darkMode: "class". Tailwind v4 liest tailwind.config.js nicht mehr von selbst. Wir haben ein Stylesheet neben einer Config-Datei mit module.exports = { darkMode: "class" } kompiliert: Ohne eine Zeile @config "./tailwind.config.js"; blieb dark: eine Media Query. Mit ihr wurde dark: zu :is(.dark *), der Form nur für Nachfahren aus der Tabelle unten. Die Config funktioniert über @config also weiterhin, aber die @custom-variant-Zeile ist kürzer, lässt dich den Selektor wählen und hält die Einstellung dort, wo der Rest deines Themes liegt.
Welchen Selektor solltest du für die Tailwind v4 Dark Mode Class nehmen?
Nimm &:where(.dark, .dark *), außer du brauchst ein Data-Attribut. Drei Formen sind verbreitet, und sie unterscheiden sich in zwei Details: ob das Element trifft, das die Klasse trägt, und wie viel Spezifität die Variante hinzufügt.
Die erste und die dritte stammen aus der Tailwind-Doku. Die zweite nutzt das Stylesheet von shadcn/ui v4 (opens in a new tab) (Stand Oktober 2026) und auch unsere eigene globals.css. Sie ist in Ordnung, solange die Klasse auf <html> sitzt und nichts <html> mit dark: stylt. Probleme macht sie an zwei Stellen: <html className="dark:bg-black"> trifft nie, und eine dunkle Insel wie <section className="dark dark:border-white/10"> bekommt ihren eigenen Rahmen nicht, während ihre Kinder umschalten. Fängst du neu an, nimm die :where-Form.
Ein Data-Attribut ist die bessere Wahl, wenn du mehr als zwei Themes hast (data-theme="dim", data-theme="contrast"). Eine Klassenliste kann nämlich zwei Theme-Klassen gleichzeitig enthalten, ein Attribut hält immer nur einen Wert.
Wie verbindest du next-themes mit Tailwind v4 Dark Mode in Next.js?
Übergib attribute="class" an den Provider, denn next-themes schreibt standardmäßig ein data-theme-Attribut. next-themes (opens in a new tab) erledigt drei Aufgaben: Es speichert die Wahl in localStorage, folgt dem System, wenn die Wahl system ist, und fügt ein kleines Skript ein, das die Klasse setzt, bevor die Seite gezeichnet wird. Provider und Root Layout für Tailwind v4 Dark Mode in Next.js:
Stand Oktober 2026 zeigt die README von next-themes (opens in a new tab) noch das Setup für Tailwind v3 (darkMode: 'selector' in tailwind.config.js) und erwähnt v4 nicht. Das Gegenstück in v4 ist die @custom-variant-Zeile von oben, sonst nichts.
Zwei Details übersieht man leicht:
color-schemegibt es gratis dazu. next-themes setzt außerdemstyle="color-scheme: dark"auf<html>(die PropenableColorScheme, standardmäßig an). Das macht Scrollbars, Datumsfelder und Autofill dunkel. Ohne next-themes ergänzt du die v4-Utilityscheme-darkodercolor-scheme: darkin deinem.dark-Block.- Der Hintergrund gehört auf
body. Setzbg-backgroundauf<body>, nicht auf<html>mit einerdark:-Klasse, wegen der Spezifität aus der Tabelle.
Das komplette Projekt-Setup, von create-next-app bis zur ersten Komponente, steht in unserem Guide zu einer Next.js Component Library. Warum suppressHydrationWarning auf <html> steht und was du tust, wenn die Konsole trotzdem meckert, ist das ganze Thema des Artikels zum next-themes Hydration Error.
Tailwind v4 Dark Mode funktioniert nicht: Woran liegt das?
Meist fehlt die Variante, oder sie passt nicht zu dem, was der Toggle schreibt. Geh die Liste der Reihe nach durch; zu jeder Ursache gibt es einen schnellen Check.
- Keine
@custom-variant-Zeile. Such im kompilierten CSS in den DevTools nachprefers-color-scheme. Stehen deinedark:-Regeln in dieser Media Query, fehlt die Variante oder sie liegt in einer Datei, die nicht importiert wird. - Variante und Toggle passen nicht zusammen. Die Variante sucht nach
.dark, während next-themesdata-theme="dark"schreibt, oder umgekehrt. Schau dir<html>nach einem Umschalten an und vergleich. - Ein übrig gebliebener Media-Block. Das Stylesheet von
create-next-appbringt@media (prefers-color-scheme: dark) { :root { ... } }mit. Diese Variablen folgen dem Betriebssystem, egal was deine Klasse sagt, also wird die Seite halb dunkel. Verschieb die Werte in einen.dark-Block und lösch den Media-Block. dark:auf dem Element, das die Klasse trägt. Mit:is(.dark *)stylt ein Element nur seine Nachfahren. Wechsle zur:where-Form oder verschieb die Klasse weiter nach oben.- Tokens zu früh aufgelöst. Sitzt
.darkauf einem Container statt auf<html>, bilde deine Variablen mit@theme inlineab. Ohneinlinewird--color-surfaceeinmal auf:rootaufgelöst, und ein verschachteltes.darkerreicht es nicht. Der Guide zu Tailwind v4 Design Tokens zeigt den Unterschied. - Bilder folgen dem Betriebssystem.
<picture><source media="(prefers-color-scheme: dark)">ist ebenfalls eine Media Query und ignoriert deinen Toggle. Render beide Bilder und blende eines mitdark:hiddenundhidden dark:blockaus. - Aus Strings gebaute Klassen.
dark:${color}erreicht den Scanner von Tailwind nie. Schreib vollständige Klassennamen oder gib eine CSS-Variable weiter. - Die Browser-Toolbar folgt dem Betriebssystem. Ein
themeColor-Array imviewport-Export von Next.js mit Einträgenmedia: "(prefers-color-scheme: dark)"ist eine weitere Media Query. Safari kann dann eine weiße Toolbar über einer Seite zeigen, die jemand auf dunkel umgestellt hat. Nimm eine neutrale Farbe oder aktualisiere dastheme-color-Meta-Tag aus deinem Toggle.
Wie gestaltest du Dark Mode, statt ihn zu invertieren?
Gib jeder Farbe eine Rolle und wähl den dunklen Wert jeder Rolle von Hand. In Tailwind v4 heißt das: CSS-Variablen in :root, dieselben Namen noch einmal in .dark und ein @theme inline-Block, der daraus Utilities macht:
Das sind die echten Werte aus unserer app/globals.css, gekürzt auf sechs Rollen. Eine Komponente, die auf diesen Tokens aufbaut, kommt ganz ohne dark:-Klassen aus:
Schau dir an, was die dunklen Werte tun, denn eine automatische Invertierung würde nichts davon leisten:
- Flächen werden heller, je weiter sie nach vorn kommen. Die Seite ist schwarz, ein erhöhtes Panel
#1A1A1A, ein Popover#262626. Invertierst du weiße Panels auf einer hellgrauen Seite, bekommst du dunkle Panels auf einer helleren Seite, und das wirkt verkehrt herum. - Status-Text ändert sich, Status-Flächen nicht. Die blassgrüne Fläche wird fast schwarz, und ihr Text geht von
#166534auf#4ADE80, weil dunkelgrüner Text auf dunkler Fläche beim Kontrast durchfällt. Volle Flächen mit weißem Text bleiben in beiden Themes gleich. - Schatten wechseln die Technik. Ein Schlagschatten ist auf Schwarz unsichtbar, also setzt Elevation im Dark Mode stattdessen auf eine beleuchtete Oberkante und eine Haarlinie.
Die Card zeigt alle drei. Schalte das Theme der Seite mit dem Toggle im Header um, während du sie anschaust:
$ npx wingo-ui@latest add cardTones tragen dieselbe Idee in die Props. Jede Wingo-UI-Komponente mit tone-Prop färbt sich über fünf Variablen aus tones (--tone, --tone-foreground, --tone-soft, --tone-soft-foreground, --tone-border), und jede davon zeigt auf ein semantisches Token. Ein Badge mit tone="success" stimmt deshalb in beiden Themes ohne eine einzige dark:-Klasse, und eine eigene color-Prop läuft per color-mix() in Richtung --foreground, passt sich also ebenfalls an. Das komplette Token-Set mit einem Kontrast-Check, den du in CI laufen lassen kannst, steht im Pillar-Artikel zu Tailwind v4 Design Tokens.
Wann solltest du trotzdem dark:-Klassen schreiben?
Für Dinge, die ein Token nicht beschreiben kann. Eine gute Regel: Bräuchten zwei verschiedene Komponenten dieselbe dark:-Klasse, sollte sie stattdessen ein Token sein. Übrig bleibt meistens:
- Assets. Ein Logo mit schwarzem Schriftzug, ein Screenshot, eine Illustration für eine weiße Seite: Render beide Versionen und nutze
dark:hidden/hidden dark:block. - Das Icon des Theme Toggles selbst. Sonne im hellen, Mond im dunklen Modus: Genau dafür ist
dark:gedacht, und es hat einen Nebeneffekt, den du weiter unten siehst. - Eine einmalige Korrektur.
dark:opacity-80auf einem hellen Foto, damit es nicht blendet.
Sammelt eine Komponentendatei dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-800, fehlt ein Token. Diese Klassen laufen auseinander, sobald jemand das nächste Mal die Palette anpasst.
Wie baust du einen Tailwind v4 Dark Mode Toggle?
Lies und setz das Theme über next-themes in einer Client Component und zeichne das Icon mit dark:-Klassen. Was einen guten Toggle von einem flackernden unterscheidet: Der Server kennt das Theme nicht, also muss alles, was der Toggle vor der Hydration rendert, aus CSS kommen. Genau das leistet ein Icon aus dark:-Klassen: Das Server-HTML enthält beide Zustände, und die Klasse, die next-themes vor dem Paint setzt, wählt einen davon aus.
Unser Theme Toggle ist so gebaut. Sein erster Render zeichnet Sonne und Mond aus dark:-Klassen, und die animierte Version übernimmt, sobald React das Theme kennt. Wir haben die Vorschau mit blockierten JavaScript-Bundles geladen und nur das Inline-Theme-Skript erlaubt: Das Server-HTML zeigte auf einem dunklen Gerät den Mond und auf einem hellen die Sonne. Es gibt ihn als Icon Button, als Segmented Control oder als Dropdown, und er schaltet auch auf System:
$ npx wingo-ui@latest add theme-toggleInstallier ihn mit npx wingo-ui@latest add theme-toggle. Er liest useTheme() aus dem Provider von oben (oder nimmt value und onValueChange, wenn du das Theme selbst speicherst), und sein Icon vor der Hydration nutzt dark:-Klassen. Der Provider muss also die Klasse schreiben, die deine Variante erwartet. Mit variant="segmented" und includeSystem wird er zur Zeile Light / Dark / System auf einer Einstellungsseite. Theme Toggle gehört zu Pro (siehe Preise). Willst du einen kostenlosen, nutzt der Button mit zwei Icons im Guide zur Next.js Component Library denselben dark:hidden-Trick in etwa 20 Zeilen.
Was solltest du als Nächstes tun?
Füg die @custom-variant-Zeile ein, setz attribute="class" auf den Provider und durchsuch deine Komponenten mit grep nach dark:: Jede wiederholte Klasse ist ein Token, das auf seinen Namen wartet. Öffne dann die Seite mit blockiertem JavaScript in den DevTools und prüf, ob das Server-HTML schon das richtige Theme zeigt. Mehr Artikel zu diesem Stack findest du unter Next.js und Tailwind CSS.
Häufige Fragen
Warum funktioniert Tailwind v4 Dark Mode nicht mit meinem Toggle?
Ohne eine @custom-variant-dark-Zeile kompiliert Tailwind v4 dark: zu @media (prefers-color-scheme: dark), folgt also dem Betriebssystem und ignoriert jede Klasse, die du setzt. Füg @custom-variant dark (&:where(.dark, .dark *)); nach @import "tailwindcss" ein und prüf, ob dein Toggle genau die Klasse oder das Attribut schreibt, nach dem die Variante sucht.
Funktioniert darkMode: "class" in tailwind.config.js noch mit Tailwind v4?
Nur wenn das Stylesheet die Datei per @config lädt, denn Tailwind v4 liest tailwind.config.js nicht mehr von selbst. Eine @custom-variant-Zeile im CSS erledigt dasselbe und ist der vorgesehene Weg in v4.
Was ist der Unterschied zwischen :is(.dark *) und :where(.dark, .dark *)?
Die :where-Form trifft auch das Element, das die dark-Klasse trägt, und fügt keine Spezifität hinzu, während :is(.dark *) nur dessen Nachfahren trifft und die Spezifität einer Klasse hinzufügt. Liegt die Klasse auf html und deine Styles auf body oder darunter, verhalten sich beide gleich.
Soll next-themes mit Tailwind v4 attribute class oder data-theme nutzen?
Beides geht, solange die Variante dazu passt. next-themes nutzt standardmäßig data-theme, also übergib entweder attribute="class" an den ThemeProvider oder schreib die Variante als @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));.
Brauche ich dark:-Klassen auf jeder Komponente?
Nein. Leg Farben in CSS-Variablen, definier sie in einem .dark-Block neu und bilde sie mit @theme inline auf Utilities ab; bg-surface oder text-muted-foreground wechseln dann mit dem Theme. Heb dir dark: für echte Ausnahmen auf, etwa ein Logo oder einen Screenshot.
- Tailwind CSS
- Dark Mode
- Next.js
- Design Tokens


