# 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.

> Pagina: https://ui.eneawork.it/blog/shadcn-dark-mode-next-themes
> Markdown: https://ui.eneawork.it/blog/shadcn-dark-mode-next-themes.md

*2026-11-01*

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 catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. 1 blocco su 1.556 contiene «dark» nell'id, contato da data/blocks.json lo stesso giorno: il tema scuro è una variante dei componenti, non un blocco a sé.

## 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.

## FAQ

### 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.
