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

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:
- Das
<html>-Element. next-themes fügt ein Inline-Skript ein, das während des Parsens läuft undclass="dark"sowiestyle="color-scheme: dark"auf<html>schreibt, bevor React hydriert. React sieht Attribute, die der Server nie geschickt hat. - Alles, was du aus
useTheme()renderst. EinresolvedTheme === "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:
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:
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, woThemeProvidergerendert wird. In einer Page oder einem verschachtelten Layout kann es hinter Inhalt landen, den der Browser schon gezeichnet hat. attributepasst zu deinem CSS. next-themes schreibt standardmäßigdata-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-colorsaufbodyoder auf jeder Card macht aus einem Theme-Wechsel ein langsames Verschmieren, das wie Flackern wirkt.disableTransitionOnChangeschaltet 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:
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:
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:
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:
$ npx wingo-ui@latest add theme-toggleDie 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:
$ npx wingo-ui@latest add theme-toggleInstallier 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.
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


