React

Dark mode con next-themes

ThemeProvider con attribute="class", una variante dark per Tailwind 4 e un toggle che legge resolvedTheme: senza questi tre pezzi il tema non cambia.

In sintesi

  • `attribute="class"` scrive `.dark` sull'elemento html; Tailwind 4 ha bisogno di `@custom-variant dark` per leggere quella classe.
  • Leggi `resolvedTheme`, non `theme`: con `defaultTheme="system"` il valore può essere «system» e il toggle si rompe.
  • `suppressHydrationWarning` sull'elemento html evita il mismatch tra server e client.

Risposta breve: avvolgi l'app in ThemeProvider con attribute="class", defaultTheme="system" e enableSystem, aggiungi @custom-variant dark al CSS di Tailwind 4 e leggi resolvedTheme nel toggle. next-themes applica la classe prima del primo paint.

Il provider, una volta sola

Il provider va montato sopra ogni cosa che legge il tema. Con l'App Router il file che lo contiene è un client component, richiamato dal layout server. disableTransitionOnChange disattiva le transizioni CSS durante il cambio, altrimenti il colore di sfondo si anima mentre la pagina cambia e il risultato sembra un glitch.

// app/providers.tsx
"use client";

import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    
      {children}
    
  );
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    
      
        {children}
      
    
  );
}

suppressHydrationWarning serve perché il server non può sapere quale tema ha scelto l'utente: la classe sull'elemento html cambia prima che React idrati, e senza l'attributo la console segnala un mismatch. Non è un permesso generale: vale solo per quell'elemento.

Tailwind 4 non legge .dark da solo

In Tailwind 4 la variante dark: segue di default prefers-color-scheme. Con next-themes la scelta è una classe, quindi la variante va ridefinita nel CSS globale. Senza questa riga il pulsante cambia la classe sull'html e nessuna utility dark: si attiva.

@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

I valori dei token per i due temi stanno in :root e .dark, con la mappatura in @theme inline. La procedura completa è in come tematizzare shadcn/ui.

Il toggle che non salta

Il tema salvato vive in localStorage, che il server non vede. Il primo render del client deve quindi mostrare qualcosa di neutro, e solo dopo l'idratazione può leggere il valore vero. resolvedTheme risolve «system» nella preferenza effettiva; theme può valere «system» e rompere un confronto con «dark».

"use client";

import { useEffect, useState } from "react";
import { useTheme } from "next-themes";

export function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  if (!mounted) {
    return ;
  }

  const isDark = resolvedTheme === "dark";

  return (
     setTheme(isDark ? "light" : "dark")}
    >
      {isDark ? "Chiaro" : "Scuro"}
    
  );
}

Perché non c'è il flash

next-themes inserisce uno script inline nel che legge localStorage e applica la classe prima che il browser dipinga il body. È lo stesso principio descritto in dark mode senza flash, con una differenza: qui lo script lo genera la libreria, quindi non va copiato a mano. Se il flash compare lo stesso, il provider è montato troppo in basso nell'albero o manca suppressHydrationWarning.

Limite dichiarato

Il tema cambia solo ciò che legge i token. Un componente con colori fissi, per esempio bg-white o text-zinc-900, resta chiaro anche in .dark: next-themes non riscrive le classi. Il catalogo, misurato il 1 novembre 2026, ha un solo blocco con «dark» nell'id, quindi la maggior parte degli esempi va verificata a mano nel tema scuro.

Dove continuare

Nel catalogo trovi la libreria shadcn, il record delle icone shadcn e un blocco con variante scura. Per il resto leggi che cosa è shadcn/ui e dark mode senza un secondo design system; il confronto tra strategie è in temi shadcn gratis o premium.

Domande frequenti

Perché dark: non funziona con next-themes?

Perché in Tailwind 4 la variante segue prefers-color-scheme. Con attribute="class" serve @custom-variant dark (&:is(.dark *)) nel CSS globale, altrimenti la classe .dark non attiva nessuna utility.

Perché il tema lampeggia al caricamento?

Il provider è montato troppo in basso, oppure manca suppressHydrationWarning sull'elemento html. next-themes deve stare nel layout radice e scrivere la classe prima del primo paint.

Devo usare theme o resolvedTheme?

resolvedTheme, quando devi sapere quale tema è attivo davvero. theme può valere "system"; resolvedTheme restituisce "light" o "dark" in base alla preferenza del sistema.

Serve un componente montato per il toggle?

Sì, se il toggle mostra un'icona o un testo che dipende dal tema. Il server non conosce localStorage, quindi il primo render deve essere neutro e il valore vero arriva dopo l'idratazione.

Il tema scuro cambia anche i componenti shadcn?

Sì, se leggono i token semantici. Un componente con classi fisse come bg-white resta chiaro: il tema non riscrive le classi, cambia solo il valore delle variabili.