Next.js Dark Mode: next-themes – der vollständige Leitfaden

Als ich zum ersten Mal Dark Mode in einem Next.js-Projekt umsetzte, bin ich richtig reingefallen. Beim Laden blitzte kurz helles Weiß auf, dann wechselte die Seite in den Dark Mode – dieses Flackern war zum Verzweifeln. Nutzer kommentierten, dass ihnen „fast die Augen wehtun“ – da wurde mir klar, wie ernst das Problem ist.
Ich habe mehrere Ansätze ausprobiert: selbst gebaut, die Bibliothek use-dark-mode, unzählige Tutorials – am Ende war next-themes die Rettung. Heute nutze ich es überall: flackerfrei, einfach konfiguriert, System-Theme funktioniert einwandfrei. In diesem Artikel teile ich die Fallstricke und Lösungen, die ich unterwegs gefunden habe.
Warum ich schließlich next-themes gewählt habe
Anfangs überlegte ich, ob ich die Theme-Logik selbst schreiben soll. localStorage lesen, eine Klasse setzen – klingt einfach. Mit Next.js und Server-Side Rendering wird es aber schnell komplex.
Ich habe mehrere Wege getestet:
Eigene Lösung: Das größte Problem ist Flackern. Beim SSR kennt der Server die Theme-Präferenz nicht und rendert das helle Standard-Theme. Erst nach der Hydration liest der Client localStorage – der Wechsel zu Dark Mode ist sichtbar.
use-dark-mode: Solide Bibliothek, aber nicht für Next.js gemacht – in SSR-Szenarien gibt es weiterhin Kompatibilitätsprobleme.
theme-ui: Sehr mächtig, für reinen Dark-Mode-Wechsel aber zu schwer und mit größerem Bundle.
Dann entdeckte ich next-themes: über 6.000 Stars auf GitHub, speziell für Next.js, null Abhängigkeiten, unter 1 KB gzipped. Entscheidend: wirklich flackerfrei, System-Theme out of the box, automatische Persistenz und gute TypeScript-Unterstützung.
Vollständige Implementierung
Abhängigkeit installieren
Wie üblich zuerst das Paket:
npm install next-themes
Alternativ mit pnpm oder yarn:
pnpm add next-themes
# oder
yarn add next-themes
ThemeProvider-Komponente erstellen
Als Nächstes eine Provider-Komponente – ich lege sie meist unter providers oder components ab.
Datei providers/theme-provider.tsx:
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
Wichtig: 'use client' setzen, weil next-themes Browser-APIs braucht. Mein erster Stolperstein: ohne diese Direktive gab es Hydration-Fehler.
In Layout integrieren
ThemeProvider ins Root-Layout – bei App Router (Next.js 13+) typischerweise app/layout.tsx:
import { ThemeProvider } from '@/providers/theme-provider'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="de" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Die wichtigsten Optionen im Detail:
attribute="class": next-themes ändert die class am <html>-Element – passt perfekt zu Tailwinds dark:-Präfix.
defaultTheme="system": Standardmäßig System-Theme. Beim ersten Besuch wird die OS-Präferenz erkannt.
enableSystem: System-Theme-Erkennung aktivieren – sonst wirkt defaultTheme="system" nicht.
disableTransitionOnChange: Übergänge beim Wechsel deaktivieren. Optional; ich empfehle es, weil sonst alle Elemente gleichzeitig animieren – wirkt oft unruhig.
suppressHydrationWarning: Am <html>-Tag unbedingt setzen. next-themes ändert die Klasse vor der Hydration – ohne dieses Attribut warnt React.
Theme-Toggle-Button erstellen
Mit dem Provider können Sie einen Umschalter bauen – components/theme-toggle.tsx:
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Theme wechseln"
>
{theme === 'dark' ? '🌞' : '🌙'}
</button>
)
}
Kleiner Trick: Vor dem Mount null zurückgeben. Beim SSR gibt es keine Theme-Information – direktes Rendern führt zu Hydration-Mismatch. Erst nach mounted liefert useTheme das aktuelle Theme zuverlässig.
Für drei Zustände (light / dark / system):
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
const cycleTheme = () => {
if (theme === 'light') setTheme('dark')
else if (theme === 'dark') setTheme('system')
else setTheme('light')
}
const getIcon = () => {
if (theme === 'light') return '🌞'
if (theme === 'dark') return '🌙'
return '💻'
}
return (
<button
onClick={cycleTheme}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
>
{getIcon()}
</button>
)
}
Flackern im Detail
Was mich damals zum Tiefergraben brachte, war genau dieses Flackern. Erst nach längerer Recherche war mir die Ursache klar.
Wie FOUC entsteht
FOUC (Flash of Unstyled Content) tritt bei Next.js-Dark-Mode häufig auf – weil SSR und Client-Zustand auseinanderlaufen.
Beim SSR gibt es kein window, kein localStorage, keine System-Theme-Info. Der Server rendert meist das helle Standard-Theme.
Im Browser startet die Hydration: React macht statisches HTML interaktiv, liest localStorage, sieht „dark“ und setzt die dark-Klasse am DOM.
Diese Änderung triggert ein Re-Render – alle Styles wechseln von hell zu dunkel. Das ist das Flackern.
Die Lösung von next-themes
next-themes injiziert ein blockierendes Script in <head>. Es läuft vor dem ersten Paint, liest localStorage und setzt sofort die passende Klasse am <html>.
Ungefähr so:
(function() {
try {
const theme = localStorage.getItem('theme')
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const currentTheme = theme || systemTheme
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark')
}
} catch (e) {}
})()
Weil das Script synchron läuft und das Rendern blockiert, steht die richtige Klasse da, bevor Inhalt sichtbar wird – CSS greift von Anfang an, kein Flackern.
Häufige Fehlkonfigurationen
Typische Stolpersteine:
suppressHydrationWarning vergessen:
Ohne dieses Attribut am <html>-Tag erscheint oft:
Warning: Prop `className` did not match. Server: "" Client: "dark"
Funktional meist unkritisch, aber nervig.
ThemeProvider falsch platziert:
ThemeProvider in einer Server Component oder außerhalb von body – beides problematisch. Er muss den Seiteninhalt umschließen und Client Component sein.
Falsche Tailwind-Konfiguration:
module.exports = {
darkMode: 'media',
}
media folgt nur dem System-Theme, manueller Wechsel geht nicht. Besser:
module.exports = {
darkMode: 'class',
}
Persistenz und System-Theme
Persistenz-Mechanismus
next-themes speichert die Auswahl standardmäßig in localStorage unter dem Schlüssel 'theme' – automatisch, ohne Extra-Code.
Eigenen Schlüssel setzen:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="my-theme"
>
{children}
</ThemeProvider>
Manchmal brauchen Sie Cookies statt localStorage – z. B. wenn der Server das Theme kennen soll:
- Cookie in Middleware lesen und an Response hängen
- SSR nach Cookie rendern
- Client synchronisiert Cookie und localStorage
Für die meisten Projekte reicht die Standard-Lösung von next-themes.
System-Theme-Follow
Mit enableSystem reagiert next-themes auf OS-Wechsel zwischen Hell und Dunkel – solange das App-Theme system ist.
Technisch über die Media Query prefers-color-scheme:
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
// Theme-Wechsel-Logik
})
Manuelle Auswahl überschreibt das System: OS hell, Website dunkel – next-themes merkt sich das für den nächsten Besuch.
Mehrere Themes
Neben Hell/Dunkel unterstützt next-themes beliebig viele Themes – z. B. Lila oder Grün:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
themes={['light', 'dark', 'purple', 'green']}
>
{children}
</ThemeProvider>
Passende CSS-Definitionen:
.purple {
--background: #f3e8ff;
--foreground: #581c87;
}
.green {
--background: #dcfce7;
--foreground: #14532d;
}
Mit CSS-Variablen sehr flexibel.
Praxis-Tipps und häufige Probleme
Mit Tailwind CSS
In tailwind.config.js:
module.exports = {
darkMode: 'class',
// weitere Konfiguration...
}
Dann frei mit dark: arbeiten:
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 className="text-2xl font-bold">Überschrift</h1>
<p className="text-gray-600 dark:text-gray-400">Absatztext</p>
</div>
Tailwinds dark:-Variante greift, wenn <html> die Klasse dark hat – passt exakt zu next-themes.
Animation und Übergänge
disableTransitionOnChange empfehle ich zu aktivieren. Viele transition-Regeln in CSS lassen beim Theme-Wechsel alles gleichzeitig animieren – wirkt chaotisch.
Wenn Sie Übergänge wollen:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange={false}
>
{children}
</ThemeProvider>
Global in CSS:
* {
transition: background-color 0.2s ease, color 0.2s ease;
}
Ich finde ohne Übergang oft klarer – aber Geschmackssache.
TypeScript-Typen
next-themes ist gut typisiert. Eigene Theme-Typen:
import { useTheme } from 'next-themes'
type Theme = 'light' | 'dark' | 'purple'
export function useCustomTheme() {
const { theme, setTheme } = useTheme()
return {
theme: theme as Theme,
setTheme: (theme: Theme) => setTheme(theme),
}
}
So gibt es Autocomplete und keine ungültigen Theme-Namen.
Fehlerbehebung
Problem 1: Theme wechselt, Styles nicht
Prüfen Sie:
- Tailwind
darkMode: 'class' dark:-Präfix oder.dark-Selektor in CSS- Im DevTools: hat
<html>die erwartete Klasse?
Problem 2: Nach Reload kurzes Flackern
Mögliche Ursachen:
suppressHydrationWarningam<html>fehlt- ThemeProvider falsch platziert
- Störende Scripts (z. B. Analytics)
Problem 3: System-Theme-Follow funktioniert nicht
Prüfen Sie:
enableSystem={true}- Browser unterstützt
prefers-color-scheme(moderne Browser: ja) - Aktuelles Theme ist
system(nach manuellem Wechsel evtl.lightoderdark)
Fazit
Vom Flackern-Frust zur reibungslosen Dark-Mode-Umsetzung – next-themes hat den Unterschied gemacht. Nicht nur technisch, sondern für die Nutzererfahrung.
Kernpunkte:
next-themeslöst Next.js-Dark-Mode-Flackern nahezu ohne KonfigurationsuppressHydrationWarningam<html>, ThemeProvider als Client Component- Tailwind:
darkMode: 'class' - Toggle erst nach
mountedrendern - System-Theme und manueller Wechsel können koexistieren
Wenn Sie next-themes noch nicht nutzen: ausprobieren lohnt sich. Die Doku ist klar: github.com/pacocoursey/next-themes
Bauen Sie jetzt flüssigen Dark Mode in Ihr Next.js-Projekt ein – Ihre Nutzer werden es merken.
Next.js Dark Mode – vollständiger Implementierungsablauf
Flackerfreier Dark Mode mit next-themes, inklusive System-Theme-Follow und manuellem Wechsel
⏱️ Estimated time: 30 min
- 1
Step 1: next-themes installieren
Abhängigkeit installieren:
• npm install next-themes
Alternativ mit anderen Paketmanagern:
• pnpm add next-themes
• yarn add next-themes
Hinweis: next-themes ist abhängigkeitsfrei und sehr klein - 2
Step 2: ThemeProvider konfigurieren
Im Root-Layout einbinden:
• Datei providers.tsx anlegen (‚use client‘)
• children mit ThemeProvider umschließen
• In app/layout.tsx importieren und nutzen
Wichtige Optionen:
• attribute="class": Theme per CSS-Klasse wechseln
• enableSystem: System-Theme-Follow aktivieren
• storageKey: localStorage-Schlüssel
Hinweis: ThemeProvider muss eine Client Component sein - 3
Step 3: Tailwind CSS konfigurieren
In tailwind.config.js:
• darkMode: 'class' setzen
• Tailwind schaltet dann per class am html-Tag um
Beispiel:
module.exports = {
darkMode: 'class',
// ... weitere Konfiguration
}
Dark Styles mit dark:-Präfix:
className="bg-white dark:bg-gray-900" - 4
Step 4: Hydration-Warnungen beheben
Am html-Tag ergänzen:
• suppressHydrationWarning
• Vermeidet Warnungen bei unterschiedlichem Server-/Client-Theme
In layout.tsx:
<html lang="de" suppressHydrationWarning>
<body>{children}</body>
</html>
So verschwinden Next.js-Hydration-Warnungen - 5
Step 5: Theme-Toggle-Button erstellen
useTheme-Hook nutzen:
• ThemeToggle-Komponente (‚use client‘)
• useTheme() für theme und setTheme
• Erst nach mounted rendern
Beispiel:
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Theme wechseln
</button> - 6
Step 6: Testen und verifizieren
Testpunkte:
• Manueller Theme-Wechsel (ohne Flackern)
• System-Theme-Follow
• Theme bleibt nach Reload erhalten
• Konsistentes Theme auf allen Seiten
Checkliste:
• Kein Flackern beim Laden
• Flüssiger Theme-Wechsel
• localStorage speichert korrekt
• System-Theme-Wechsel wird mitgeführt
FAQ
Warum flackert die Seite beim Laden?
Worin unterscheidet sich next-themes von anderen Theme-Bibliotheken?
Wie aktiviere ich System-Theme-Follow?
Warum brauche ich suppressHydrationWarning?
Warum den Toggle erst nach mounted rendern?
Wie passe ich die Theme-Wechsel-Logik an?
Welche Themes unterstützt next-themes?
7 Min. Lesezeit · Veröffentlicht am: 20. Dez. 2025 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
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
Next.js Produktions-Monitoring: Sentry, Logs und Alerts in der Praxis
Schritt-für-Schritt Next.js Produktions-Monitoring: Sentry-Integration, Log-Management, Performance-Monitoring und Alert-Konfiguration. App Router mit kopierbaren Code-Vorlagen.
Teil 43 von 51
Nächster
Next.js + Tailwind CSS Best Practices: Vollständiger Leitfaden von Konfiguration bis Dark Mode (2025)
Aktueller Next.js + Tailwind CSS v4 Praxisleitfaden 2025: zu lange Klassennamen, Custom Theme, Dark Mode und Performance – von 500 KB auf 50 KB mit echten Fallbeispielen und vollständigen Codebeispielen.
Teil 45 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


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