Design wechseln

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

Easton editorial illustration: state-management shelf

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:

  1. Cookie in Middleware lesen und an Response hängen
  2. SSR nach Cookie rendern
  3. 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:

  • suppressHydrationWarning am <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. light oder dark)

Fazit

Vom Flackern-Frust zur reibungslosen Dark-Mode-Umsetzung – next-themes hat den Unterschied gemacht. Nicht nur technisch, sondern für die Nutzererfahrung.

Kernpunkte:

  1. next-themes löst Next.js-Dark-Mode-Flackern nahezu ohne Konfiguration
  2. suppressHydrationWarning am <html>, ThemeProvider als Client Component
  3. Tailwind: darkMode: 'class'
  4. Toggle erst nach mounted rendern
  5. 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. 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. 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. 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. 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. 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. 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?
Beim SSR kennt der Server die Theme-Präferenz nicht und rendert das Standard-Theme. Erst nach der Client-Hydration liest JavaScript localStorage und wechselt – das verursacht Flackern. next-themes injiziert ein Script vor dem Rendern, liest das Theme vorab und behebt das Problem.
Worin unterscheidet sich next-themes von anderen Theme-Bibliotheken?
next-themes ist für Next.js gebaut, löst SSR-Flackern, hat null Abhängigkeiten und ist klein (<1 KB). use-dark-mode ist nicht Next.js-spezifisch und hat SSR-Probleme. theme-ui ist mächtig, aber für reinen Dark-Mode-Wechsel Overkill.
Wie aktiviere ich System-Theme-Follow?
In ThemeProvider enableSystem={true} setzen – next-themes erkennt die System-Präferenz automatisch. Manuelle Auswahl überschreibt das System-Theme. Drei Modi: light, dark, system.
Warum brauche ich suppressHydrationWarning?
Server und Client kennen beim ersten Render oft unterschiedliche Themes – localStorage ist nur clientseitig. suppressHydrationWarning sagt React, dass die Abweichung erwartet ist, und unterdrückt die Warnung.
Warum den Toggle erst nach mounted rendern?
Um Hydration-Mismatch zu vermeiden. Beim SSR ist localStorage nicht verfügbar; ein sofort gerenderter Button erzeugt unterschiedliches HTML auf Server und Client. Nach mounted rendert die Komponente nur clientseitig.
Wie passe ich die Theme-Wechsel-Logik an?
Mit setTheme aus useTheme: z. B. setTheme(theme === 'dark' ? 'light' : 'dark'). Direkt setzen: setTheme('dark'), setTheme('light') oder setTheme('system').
Welche Themes unterstützt next-themes?
Standard: light und dark. Über die themes-Prop des ThemeProvider beliebig erweiterbar, z. B. themes={['light', 'dark', 'blue', 'green']}. Jedes Theme entspricht einer CSS-Klasse.

7 Min. Lesezeit · Veröffentlicht am: 20. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog