Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Next.js und Tailwind CSS
    4. next-themes Hydration Error: Flackern in Next.js beheben
    Read in English
    1. Blog
    2. Next.js und Tailwind CSS
    3. next-themes Hydration Error: Flackern in Next.js beheben
    Next.js und Tailwind CSS
    Next.js und Tailwind CSS

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

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

    Auf dieser Seite

    0%
    1. Warum verursacht next-themes einen Hydration Error?
    2. Reicht suppressHydrationWarning auf html?
    3. Warum flackert mein Dark Mode in Next.js?
    4. Was bedeutet „Encountered a script tag while rendering React component“?
    5. Wie renderst du einen Theme Toggle, der beim ersten Paint stimmt?
      1. Wann brauchst du ein mounted-Flag?
      2. Was der Theme Toggle von Wingo UI macht
    6. Was solltest du als Nächstes tun?
    7. Häufige Fragen
      1. Warum verursacht next-themes einen Hydration Error?
      2. Ist suppressHydrationWarning auf dem html-Tag mit next-themes sicher?
      3. Wie stoppe ich das Dark-Mode-Flackern in Next.js?
      4. Was bedeutet Encountered a script tag while rendering React component bei next-themes?
      5. Brauche ich für einen Theme Toggle einen mounted-State?

    Kurz gesagt

    Ein next-themes Hydration Error entsteht, weil der Server das Theme nicht lesen kann, denn es liegt in localStorage: Das Inline-Skript von next-themes ändert das html-Element, bevor React hydriert, und jeder Text, jedes Label und jede Klasse, die du aus useTheme() renderst, weicht zwischen Server und Browser ab. Setz suppressHydrationWarning nur auf html, halte theme-abhängiges Markup aus dem ersten Render heraus und zeichne das Icon des Toggles mit dark:-Klassen, damit schon das Server-HTML stimmt. Das Flackern verschwindet, wenn der Provider oben im Root Layout sitzt und attribute="class" zu deinem CSS passt.

    Veröffentlicht am 13. Okt. 2026

    Du fügst next-themes zu einem Next.js-Projekt mit App Router hinzu, und die Konsole füllt sich: „A tree hydrated but some attributes of the server rendered HTML didn't match“, „Hydration failed because the server rendered text didn't match the client“ und manchmal „Encountered a script tag while rendering React component“. Oder die Konsole bleibt still, und die Seite blitzt weiß auf, bevor sie dunkel wird. Jedes dieser Symptome ist ein next-themes Hydration Error mit derselben Ursache. Dieses Tutorial behebt jedes Symptom und baut dann einen Theme Toggle, der schon im Server-HTML stimmt.

    Warum verursacht next-themes einen Hydration Error?

    Weil das Theme in localStorage liegt und der Server es nicht lesen kann. Der Quellcode von next-themes (0.4.6) macht das deutlich: Der initiale State liest localStorage nur, wenn window existiert, also sind theme und resolvedTheme auf dem Server undefined. Im Browser hat schon der erste Render den gespeicherten Wert.

    Danach unterscheiden sich zwei Dinge zwischen Server-HTML und Hydration-Render:

    1. Das <html>-Element. next-themes fügt ein Inline-Skript ein, das während des Parsens läuft und class="dark" sowie style="color-scheme: dark" auf <html> schreibt, bevor React hydriert. React sieht Attribute, die der Server nie geschickt hat.
    2. Alles, was du aus useTheme() renderst. Ein resolvedTheme === "dark" ? <Moon /> : <Sun /> rendert auf dem Server, der kein Theme kennt, die Sonne und in einem dunklen Browser den Mond.

    Wir haben jedes gängige Muster mit renderToString und hydrateRoot(document) aus React 19.3, next-themes 0.4.6 und einem dunklen Farbschema nachgestellt, in Chromium über Playwright, in Development- und Production-Builds:

    Was die Seite rendertKonsole in DevelopmentProductionWas Besucher bekommen
    <html> ohne suppressHydrationWarning„A tree hydrated but some attributes ... didn't match“NichtsKorrekte Seite
    Text oder ein Icon, gewählt aus resolvedTheme„Hydration failed because the server rendered text (or HTML) didn't match the client“, dazu der Script-Tag-FehlerMinifizierter React-Fehler #418Das ganze Dokument wird auf dem Client neu gebaut
    className oder aria-label aus resolvedThemeDie Attribut-WarnungNichtsEin veralteter Wert: is-light auf einer dunklen Seite, nie korrigiert
    Das Theme hinter einem mounted-FlagNichtsNichtsEin leerer Button bis zur Hydration

    Die dritte Zeile ist die gefährliche. Production loggt nichts, und React korrigiert Attribute nach einem Mismatch nicht. Der Button behält also die helle Klasse vom Server (oder ein Screenreader liest das falsche Label vor), bis sich der Wert wieder ändert.

    Reicht suppressHydrationWarning auf html?

    Es ist die richtige Lösung für Zeile eins und für nichts sonst. Setz suppressHydrationWarning next-themes-konform so, wie es das README verlangt, auf das <html>-Element im Root Layout:

    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>
    );
    }
    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>
    );
    }

    Die React-Doku zu Hydration-Mismatches (opens in a new tab) beschreibt das Prop als Notausgang, der nur eine Ebene tief wirkt. Genau das macht es hier sicher: Es deckt die Attribute ab, die next-themes auf <html> ändert, und nichts innerhalb von <body>. In unserem Test entfernte es die Warnung aus Zeile eins und ließ Zeile zwei und drei genau so, wie sie waren. Verteil es nicht auf Komponenten, um die anderen Zeilen ruhigzustellen. Im Fall der Klasse würde es einen Bug verstecken, der so in Production landet.

    Warum flackert mein Dark Mode in Next.js?

    Ein heller Frame vor einer dunklen Seite heißt: Die Klasse kam nach dem ersten Paint. Genau das ist das Next.js Dark Mode Flackern. Damit der next-themes Flash verschwindet, muss das Skript von next-themes vor dem Inhalt laufen, den es stylt. Prüf diese Punkte der Reihe nach:

    • Der Provider steht im Root Layout, oben im <body>. Das Skript wird dort gerendert, wo ThemeProvider gerendert wird. In einer Page oder einem verschachtelten Layout kann es hinter Inhalt landen, den der Browser schon gezeichnet hat.
    • attribute passt zu deinem CSS. next-themes schreibt standardmäßig data-theme. Sucht deine Tailwind-Variante nach .dark, läuft das Skript zwar rechtzeitig, aber dein CSS ignoriert es, und die Seite bleibt hell, bis etwas anderes die Klasse setzt. Der Beitrag zum Tailwind v4 Dark Mode erklärt die Variante.
    • Keine dunklen Werte in einem Media-Block. Ein übrig gebliebener @media (prefers-color-scheme: dark)-Block aus dem Starter zeichnet zuerst das Theme des Betriebssystems, danach überschreibt deine Klasse es: ein Flackern für alle, deren Wahl vom Betriebssystem abweicht.
    • Kein Theme-Code in useEffect. Setzt du die Klasse selbst in einem Effect, läuft das nach der Hydration und ist damit per Definition zu spät.
    • Farb-Transitions. Ein transition-colors auf body oder auf jeder Card macht aus einem Theme-Wechsel ein langsames Verschmieren, das wie Flackern wirkt. disableTransitionOnChange schaltet Transitions ab, während next-themes das Theme wechselt.

    Wenn ein Icon flackert (erst erscheint die Sonne, dann der Mond), liegt die Ursache in Zeile zwei der Tabelle: ein Render, der vom Theme abhängt. Der Abschnitt zum Toggle weiter unten behebt das.

    Was bedeutet „Encountered a script tag while rendering React component“?

    React 19 loggt das im Development-Modus, wenn es ein <script>-Element auf dem Client erzeugt, statt eines aus dem Server-HTML zu hydrieren. So ein Skript läuft nie, deshalb warnt React. Die Prüfung in react-dom überspringt Datenblöcke, also Skripte, deren type kein JavaScript ist, etwa application/json.

    next-themes 0.4.6 rendert sein Skript innerhalb von ThemeProvider, die Warnung erscheint also immer, wenn der Provider auf dem Client mountet. In unserem Test kam sie direkt nach dem Text-Mismatch aus Zeile zwei: React gab die Hydration auf, baute das Dokument auf dem Client neu und erzeugte das Skript von Grund auf. Das Issue dazu bei next-themes (opens in a new tab) hat im März 2026 ein Entwickler eröffnet, der nach dem Upgrade auf Next.js 16.2.0 darauf gestoßen ist, und es ist Stand Oktober 2026 noch offen.

    Behebe es deshalb in zwei Schritten. Entferne zuerst jeden Text-Mismatch, denn der löst es oft aus. Mountet der Provider danach ganz regulär auf dem Client (ein Layout, das eine Client-Navigation zum ersten Mal rendert), markier die Client-Kopie als Datenblock. Die Server-Kopie läuft weiterhin vor dem Paint, und das Script-Element trägt schon suppressHydrationWarning, also löst der andere type keine Warnung aus:

    tsx
    // app/providers.tsx
    "use client";
    import type { ReactNode } from "react";
    import { ThemeProvider } from "next-themes";
    // die server-kopie läuft vor dem paint; eine auf dem client erzeugte kopie ist ein inerter datenblock, react warnt also nicht
    const scriptProps = typeof window === "undefined" ? undefined : { type: "application/json" };
    export function Providers({ children }: { children: ReactNode }) {
    return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange scriptProps={scriptProps}>
    {children}
    </ThemeProvider>
    );
    }

    Der Provider unserer eigenen Website nutzt dieselbe scriptProps-Zeile.

    Wie renderst du einen Theme Toggle, der beim ersten Paint stimmt?

    Teil den Toggle in das, was CSS zeigen kann, und das, was nur JavaScript weiß. Die Klasse, die next-themes vor dem Paint setzt, sagt dem CSS schon, ob die Seite dunkel ist. Das Icon kann also aus dark:-Klassen kommen, ganz ohne Theme im Render:

    tsx
    // components/theme-button.tsx
    "use client";
    import { Moon, Sun } from "lucide-react";
    import { useTheme } from "next-themes";
    import { Button } from "@/components/ui/button";
    export function ThemeButton() {
    const { resolvedTheme, setTheme } = useTheme();
    return (
    <Button
    variant="ghost"
    size="icon"
    aria-label="Theme umschalten"
    onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
    >
    <Sun className="dark:hidden" />
    <Moon className="hidden dark:block" />
    </Button>
    );
    }

    resolvedTheme wird im Click-Handler gelesen, der erst nach der Hydration läuft, also rendern Server und Browser identisches Markup. Das Label benennt die Aktion („Theme umschalten“) und hängt damit auch nicht vom State ab. Lies dort resolvedTheme, nicht theme: Auf einer Seite mit System ist theme gleich "system", also wählt theme === "dark" ? "light" : "dark" Dark auf einer Seite, die schon dunkel ist, und der erste Klick tut nichts. Der Button hier ist kostenlos, aber jeder andere Button tut es auch.

    Wann brauchst du ein mounted-Flag?

    Wenn CSS den State nicht sehen kann. Theme Toggle Hydration wird mit einer System-Option schwieriger: Eine dunkle Seite sieht gleich aus, ob jemand Dark gewählt hat oder System auf einem dunklen Gerät. Keine CSS-Regel kann also entscheiden, welches Segment eines Light / Dark / System-Controls ausgewählt sein soll. Dieser Teil muss auf den Browser warten. Es ist dasselbe Problem, das unser SSR-sicherer React use mobile Hook für die Bildschirmgröße löst. Nimm für das Flag useSyncExternalStore statt des Paars aus useState und useEffect aus dem README:

    ts
    // hooks/use-mounted.ts
    import { useSyncExternalStore } from "react";
    const subscribe = () => () => {};
    // false auf dem server und während der hydration, direkt danach true; eine später gemountete komponente liest sofort true
    export function useMounted() {
    return useSyncExternalStore(subscribe, () => true, () => false);
    }
    tsx
    // components/theme-options.tsx
    "use client";
    import { useTheme } from "next-themes";
    import { useMounted } from "@/hooks/use-mounted";
    const OPTIONS = ["light", "dark", "system"] as const;
    export function ThemeOptions() {
    const mounted = useMounted();
    const { theme, setTheme } = useTheme();
    return (
    <div role="group" aria-label="Theme" className="inline-flex gap-1 rounded-full bg-foreground/5 p-1">
    {OPTIONS.map((option) => (
    <button
    key={option}
    type="button"
    aria-pressed={mounted ? theme === option : undefined}
    onClick={() => setTheme(option)}
    className="min-h-11 rounded-full px-3 text-sm capitalize aria-pressed:bg-surface aria-pressed:shadow-raised"
    >
    {option}
    </button>
    ))}
    </div>
    );
    }

    Vor der Hydration ist keine Option gedrückt, und das ist ehrlich: Der Server weiß es nicht. Die Buttons behalten ihre Größe, also verschiebt sich nichts, wenn die Auswahl erscheint. Gegenüber der useEffect-Variante gewinnt der Hook auch bei allem, was später mountet, etwa bei diesen Optionen in einem Einstellungsdialog: Für Komponenten, die nach der Hydration mounten, nutzt React den Client-Snapshot. Sie rendern die Auswahl also schon beim ersten Commit statt erst nach einem leeren Render.

    Was der Theme Toggle von Wingo UI macht

    Unser Theme Toggle nutzt beide Techniken. Sein Symbol hat einen auto-Zustand für den Server und den Hydration-Render, komplett mit dark:-Klassen gezeichnet: Unter .dark klappen die Strahlen weg, und die Scheibe wird zur Sichel. Nach dem Mount übernimmt eine animierte Version unter einem neuen React-Key, deshalb ist die Übergabe selbst nicht animiert. aria-pressed bleibt ungesetzt, bis das Theme bekannt ist.

    Zur Prüfung haben wir die Vorschau des Toggles geladen. Alle JavaScript-Bundles waren blockiert, nur das Inline-Theme-Skript durfte laufen. Das Server-HTML zeigte auf einem dunklen Gerät den Mond und auf einem hellen die Sonne. Drück den Button hier, um den Zyklus über System zu sehen:

    Drück den Button: Er wechselt zu System, dann zu dem Theme, das dein Gerät gerade nicht zeigt, dann zum Theme deines Geräts. Ein kleines Monitor-Badge markiert System
    $ npx wingo-ui@latest add theme-toggle
    ProDoku: Theme Toggle

    Die Varianten Segmented und Dropdown folgen der System-Regel von oben. Das Segmented Control hat kein ausgewähltes Segment, bis das Theme bekannt ist, und sein Thumb blendet sich dann ein. Das Dropdown Menu setzt den Haken an der aktuellen Zeile erst nach dem Mount:

    Öffne das Menü: Light, Dark und System mit einem Haken an der aktuellen Option, auf dem Smartphone als Bottom Sheet
    $ npx wingo-ui@latest add theme-toggle
    ProDoku: Theme Toggle

    Installier ihn mit npx wingo-ui@latest add theme-toggle. Theme Toggle gehört zu Pro (siehe Preise); der ThemeButton von oben ist der kostenlose Weg.

    Was solltest du als Nächstes tun?

    Setz suppressHydrationWarning auf <html> und nirgendwo sonst. Durchsuch dann deine Client-Komponenten nach useTheme() und sortier jede Verwendung ein: Icons und Farben wandern in dark:-Klassen, der Auswahl-State kommt hinter useMounted(), und Handler lesen weiterhin resolvedTheme. Lade danach eine dunkle Seite mit blockiertem JavaScript neu und prüf, ob sie schon dunkel ist und schon den Mond zeigt. Mehr Beiträge zu diesem Stack findest du unter Next.js und Tailwind CSS, angefangen beim Pillar-Beitrag zu Tailwind v4 Design Tokens. Der Guide zu prefers reduced motion behebt dieselbe Art von Mismatch für Animationen.

    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

    Häufige Fragen

    Warum verursacht next-themes einen Hydration Error?

    Der Server hat keinen Zugriff auf localStorage, also liefert useTheme() dort undefined, während der Browser das gespeicherte Theme schon beim ersten Render liest. Alles, was du aus dem Theme renderst, etwa ein Sonnen- oder Mond-Icon oder ein aria-label, unterscheidet sich dann zwischen Server-HTML und Hydration-Render.

    Ist suppressHydrationWarning auf dem html-Tag mit next-themes sicher?

    Ja. Es unterdrückt nur Attribut-Abweichungen an genau diesem einen Element, das next-themes absichtlich vor der Hydration ändert. Auf Kinder wirkt es nicht, echte Mismatches in deinen Seiten tauchen also weiterhin auf.

    Wie stoppe ich das Dark-Mode-Flackern in Next.js?

    Rendere den ThemeProvider von next-themes oben im body des Root Layouts, gib das Attribut mit, das dein CSS erwartet (attribute="class" für eine .dark-Variante), und halte dunkle Farben aus einem prefers-color-scheme-Media-Block heraus. Dann setzt das Inline-Skript das Theme, bevor irgendetwas gezeichnet wird.

    Was bedeutet Encountered a script tag while rendering React component bei next-themes?

    React 19 loggt das im Development-Modus, wenn es ein script-Element auf dem Client erzeugt, statt eines aus dem Server-HTML zu hydrieren, und dieses Skript läuft nie. Bei next-themes erscheint es, wenn der Provider auf dem Client mountet, zum Beispiel nachdem ein Hydration Error React dazu gebracht hat, die Seite neu aufzubauen.

    Brauche ich für einen Theme Toggle einen mounted-State?

    Nur für Teile, die CSS nicht zeigen kann, etwa welche Option von Light, Dark oder System ausgewählt ist. Das Icon selbst lässt sich mit dark:-Klassen zeichnen, es stimmt also schon vor der Hydration, statt leer zu bleiben.

    • Next.js
    • Dark Mode
    • Hydration
    • React

    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

    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
    Next.js und Tailwind CSS

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

    Tailwind v4 Dark Mode per Klasse: die @custom-variant-Zeile, next-themes in Next.js, warum dark: deinen Toggle ignoriert und Tokens für zwei echte Themes.

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

    Next.js Cookie Banner: DSGVO und Consent Mode v2

    Ein Next.js Cookie Banner, der der DSGVO standhält: Zustimmen und Ablehnen gleich groß, Schalter je Kategorie, Consent Mode v2 und Embeds erst nach Zustimmung.

    SRSerban Rusu·16. Okt. 2026·12 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