Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Next.js und Tailwind CSS
    4. Tailwind v4 Dark Mode: Klassen-Toggle, Tokens, next-themes
    Read in English
    1. Blog
    2. Next.js und Tailwind CSS
    3. Tailwind v4 Dark Mode: Klassen-Toggle, Tokens, next-themes
    Next.js und Tailwind CSS
    Next.js und Tailwind CSS

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

    SR
    Serban Rusu · Gründer von Wingo UI
    13. Okt. 2026 · 9 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Warum folgt Tailwind v4 Dark Mode dem Betriebssystem statt meinem Toggle?
    2. Welchen Selektor solltest du für die Tailwind v4 Dark Mode Class nehmen?
    3. Wie verbindest du next-themes mit Tailwind v4 Dark Mode in Next.js?
    4. Tailwind v4 Dark Mode funktioniert nicht: Woran liegt das?
    5. Wie gestaltest du Dark Mode, statt ihn zu invertieren?
    6. Wann solltest du trotzdem dark:-Klassen schreiben?
    7. Wie baust du einen Tailwind v4 Dark Mode Toggle?
    8. Was solltest du als Nächstes tun?
    9. Häufige Fragen
      1. Warum funktioniert Tailwind v4 Dark Mode nicht mit meinem Toggle?
      2. Funktioniert darkMode: "class" in tailwind.config.js noch mit Tailwind v4?
      3. Was ist der Unterschied zwischen :is(.dark *) und :where(.dark, .dark *)?
      4. Soll next-themes mit Tailwind v4 attribute class oder data-theme nutzen?
      5. Brauche ich dark:-Klassen auf jeder Komponente?

    Kurz gesagt

    Tailwind v4 Dark Mode folgt standardmäßig dem Betriebssystem: dark:-Utilities werden zu einer prefers-color-scheme-Media-Query kompiliert, also ändert ein Toggle, der eine Klasse setzt, gar nichts, bis du @custom-variant dark (&:where(.dark, .dark *)); in dein CSS schreibst. In Next.js schreibt next-themes diese Klasse dann vor dem ersten Paint auf html, sofern du attribute="class" übergibst, denn sein Default ist ein data-theme-Attribut. Leg Farben in semantische Tokens, die du unter .dark neu definierst: Dann brauchen Komponenten kaum dark:-Klassen, und beide Themes sind bewusst gestaltet.

    Veröffentlicht am 13. Okt. 2026

    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:

    css
    @media (prefers-color-scheme: dark) {
    .dark\:bg-black {
    background-color: var(--color-black);
    }
    }

    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:

    css
    /* app/globals.css */
    @import "tailwindcss";
    @custom-variant dark (&:where(.dark, .dark *));

    Jetzt kompiliert dieselbe Utility zu einem Selektor:

    css
    .dark\:bg-black:where(.dark, .dark *) {
    background-color: var(--color-black);
    }

    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.

    VarianteToggle schreibtTrifft das Element mit der KlasseZusätzliche Spezifität
    &:where(.dark, .dark *)class="dark"JaKeine
    &:is(.dark *)class="dark"Nein, nur NachfahrenEine Klasse
    &:where([data-theme=dark], [data-theme=dark] *)data-theme="dark"JaKeine

    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:

    tsx
    // app/providers.tsx
    "use client";
    import type { ReactNode } from "react";
    import { ThemeProvider } from "next-themes";
    export function Providers({ children }: { children: ReactNode }) {
    return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
    {children}
    </ThemeProvider>
    );
    }
    tsx
    // app/layout.tsx
    import type { ReactNode } from "react";
    import { Providers } from "./providers";
    import "./globals.css";
    export default function RootLayout({ children }: { children: ReactNode }) {
    return (
    <html lang="en" suppressHydrationWarning>
    <body className="bg-background text-foreground">
    <Providers>{children}</Providers>
    </body>
    </html>
    );
    }

    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-scheme gibt es gratis dazu. next-themes setzt außerdem style="color-scheme: dark" auf <html> (die Prop enableColorScheme, standardmäßig an). Das macht Scrollbars, Datumsfelder und Autofill dunkel. Ohne next-themes ergänzt du die v4-Utility scheme-dark oder color-scheme: dark in deinem .dark-Block.
    • Der Hintergrund gehört auf body. Setz bg-background auf <body>, nicht auf <html> mit einer dark:-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.

    1. Keine @custom-variant-Zeile. Such im kompilierten CSS in den DevTools nach prefers-color-scheme. Stehen deine dark:-Regeln in dieser Media Query, fehlt die Variante oder sie liegt in einer Datei, die nicht importiert wird.
    2. Variante und Toggle passen nicht zusammen. Die Variante sucht nach .dark, während next-themes data-theme="dark" schreibt, oder umgekehrt. Schau dir <html> nach einem Umschalten an und vergleich.
    3. Ein übrig gebliebener Media-Block. Das Stylesheet von create-next-app bringt @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.
    4. 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.
    5. Tokens zu früh aufgelöst. Sitzt .dark auf einem Container statt auf <html>, bilde deine Variablen mit @theme inline ab. Ohne inline wird --color-surface einmal auf :root aufgelöst, und ein verschachteltes .dark erreicht es nicht. Der Guide zu Tailwind v4 Design Tokens zeigt den Unterschied.
    6. 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 mit dark:hidden und hidden dark:block aus.
    7. Aus Strings gebaute Klassen. dark:${color} erreicht den Scanner von Tailwind nie. Schreib vollständige Klassennamen oder gib eine CSS-Variable weiter.
    8. Die Browser-Toolbar folgt dem Betriebssystem. Ein themeColor-Array im viewport-Export von Next.js mit Einträgen media: "(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 das theme-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:

    css
    /* app/globals.css */
    :root {
    --background: oklch(1 0 0);
    --surface: #ffffff;
    --foreground: oklch(0.3211 0 0);
    --muted-foreground: oklch(0.52 0.0234 264.3637);
    --success-soft: #e7f6ec;
    --success-soft-foreground: #166534;
    }
    .dark {
    --background: oklch(0 0 0);
    --surface: #1a1a1a;
    --foreground: oklch(0.9219 0 0);
    --muted-foreground: oklch(0.7155 0 0);
    --success-soft: #0d2315;
    --success-soft-foreground: #4ade80;
    }
    @theme inline {
    --color-background: var(--background);
    --color-surface: var(--surface);
    --color-foreground: var(--foreground);
    --color-muted-foreground: var(--muted-foreground);
    --color-success-soft: var(--success-soft);
    --color-success-soft-foreground: var(--success-soft-foreground);
    }

    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:

    tsx
    export function PaidBadge() {
    return (
    <span className="rounded-full bg-success-soft px-2 py-0.5 text-xs font-medium text-success-soft-foreground">
    Bezahlt
    </span>
    );
    }

    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 #166534 auf #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:

    Schalte das Theme der Seite um: Die Soft Card behält ihre getönte Fläche und ihren lesbaren Titel in beiden Themes, und das Anheben beim Hover wird im Dark Mode vom Schatten zur beleuchteten Kante
    $ npx wingo-ui@latest add card
    FreeDoku: Card

    Tones 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-80 auf 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:

    Wähl Light, Dark oder System: Der Thumb gleitet, die Seite wechselt mit einem Kreis, der sich vom gedrückten Segment aus öffnet, und System folgt deinem Gerät
    $ npx wingo-ui@latest add theme-toggle
    ProDoku: Theme Toggle
    tsx
    // components/site-header.tsx
    import { ThemeToggle } from "@/components/ui/theme-toggle";
    export function SiteHeader() {
    return (
    <header className="flex h-14 items-center justify-between border-b border-border px-4">
    <span className="font-semibold">Northwind CRM</span>
    <ThemeToggle shortcut="shift+t" />
    </header>
    );
    }

    Installier 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.

    Komponenten in diesem Artikel

    • Theme Toggle

      A React theme toggle for light, dark and system mode on next-themes: an animated sun and moon button, a segmented control or a menu.

      Pro
    • Tones

      One way to color a React component: a tone or any CSS color becomes five CSS variables every variant paints with.

      Free
    • Card

      A React card component for listings, stats, settings and plans: media, header, body and footer parts, and a lift-and-press mode.

      Free

    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

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Next.js und Tailwind CSS

    Tailwind v4 Design Tokens: Farben, Dark Mode, Kontrast

    Tailwind v4 Design Tokens aus einer produktiven React-Bibliothek: @theme inline, semantische Flächen, Statusfarben, Dark Mode und ein Kontrast-Check für CI.

    SRSerban Rusu·9. Okt. 2026·20 Min. Lesezeit
    Next.js und Tailwind CSS

    next-themes Hydration Error: Flackern in Next.js beheben

    Behebe den next-themes Hydration Error und das Theme-Flackern im Next.js App Router. Plus: ein Theme Toggle, der schon beim ersten Paint richtig aussieht.

    SRSerban Rusu·13. Okt. 2026·8 Min. Lesezeit
    Next.js und Tailwind CSS

    Next.js Component Library im App Router einrichten

    Next.js Component Library im App Router einrichten, getestet mit Next.js 16.4: use client, Tokens in globals.css, Dark Mode ohne Flackern, Updates per CLI.

    SRSerban Rusu·13. Okt. 2026·8 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • Buttons & Actions
    • Inputs
    • Forms
    • Navigation
    • Overlays
    • Feedback
    • Data Display
    • Tables & Lists
    • Charts & Stats
    • Layout
    • Media
    • AI Kit
    • Text & Effects
    • Mobile
    • Commerce
    • Marketing Sections
    • Blocks
    • Hooks & Utilities

    © 2026 Wingo UI

    Wingo UI