Errori e fix
Dark mode senza flash: il tema va deciso prima del primo paint
Il lampo bianco nasce perché il tema viene applicato dopo il mount di React. La correzione è decidere la classe prima del paint, con uno script bloccante o leggendo un cookie sul server.
In sintesi
- Il tema deve essere scritto sull'elemento radice prima che il browser dipinga: un effetto React arriva troppo tardi.
- Uno script inline sincrono prima del CSS, o un cookie letto dal server, eliminano il flash alla radice.
- `color-scheme` dice al browser come colorare scrollbar e controlli nativi: senza, il tema resta a metà.
Risposta breve: il tema va deciso prima del primo paint. Uno script inline sincrono nell'<head> che scrive la classe sull'elemento radice elimina il flash; un effetto React arriva sempre dopo.
Perché lo script inline vince la corsa
Il browser costruisce il DOM, poi calcola lo stile, poi dipinge. Uno script sincrono nel <head> viene eseguito durante la costruzione, prima del calcolo dello stile: quando il CSS arriva, la classe è già sull'elemento radice. Un effetto React è invece accodato dopo il commit, quando il primo frame è già stato presentato. La differenza non è di velocità, è di ordine.
C'è un secondo canale, spesso dimenticato: il browser dipinge il fondo dell'area di disegno prima di qualsiasi regola. color-scheme, dichiarato come proprietà o come meta, dice alla UA quale fondo usare, quindi anche il primo frame senza CSS è già scuro. Senza, lo script corregge il testo e la pagina resta chiara per un frame.
Il flash, spiegato
- Il browser riceve l'HTML e disegna lo sfondo di default, chiaro.
- React scarica, esegue, monta.
- Un effetto legge la preferenza e aggiunge la classe
dark. - Il browser ridipinge: il passaggio dal bianco al nero è il flash.
Ogni passaggio dopo il primo paint è troppo tardi. La correzione deve stare prima del paint, cioè nell'HTML.
La correzione: script bloccante
try {
var stored = localStorage.getItem("theme");
var dark = stored
? stored === "dark"
: matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = dark ? "dark" : "light";
} catch (e) {}
Lo script è sincrono e sta nell'<head>, prima del CSS. Il try serve per i browser che bloccano localStorage in navigazione privata: senza, un errore blocca il resto della pagina.
La variante SSR
Se il tema è un'impostazione dell'utente, salvalo in un cookie e leggilo sul server: il primo byte di HTML contiene già la classe giusta. È la soluzione più solida perché non dipende da JavaScript.
Con Next.js, React e altri framework il valore del server e quello del client possono divergere se l'utente cambia tema: metti suppressHydrationWarning solo sull'elemento <html>, che è l'unico punto in cui la differenza è voluta.
Il passo diagnostico
La verifica non si fa a occhio su una macchina veloce. Apri gli strumenti di sviluppo, scheda Performance, attiva Screenshots e registra un caricamento con la cache disattivata e la rete rallentata. Scorri la striscia dei frame: il flash è un frame chiaro seguito da uno scuro, e la sua posizione dice se lo script è partito dopo il primo paint.
In alternativa, leggi due valori nella console subito dopo un caricamento pulito.
console.log(document.documentElement.className || "(nessuna classe)");
console.log(getComputedStyle(document.documentElement).colorScheme);
Se la classe è già presente al primo frame e il fondo resta chiaro, manca color-scheme. Se la classe arriva dopo, lo script non è nel <head> oppure è bloccato dalla policy.
Quando la correzione non funziona
Se lo script inline è bloccato dalla Content Security Policy, il tema arriva tardi comunque: serve un nonce o un hash nella policy. Un secondo caso è il CSS che usa @media (prefers-color-scheme: dark) e la classe .dark insieme: le due regole si sovrascrivono e il risultato dipende dall'ordine. Scegli una sola fonte di verità.
Quando la correzione comune peggiora le cose
La scorciatoia più diffusa è nascondere la pagina finché React non ha montato, con visibility: hidden sul body e un effetto che la riporta visibile. Il flash sparisce e il costo si sposta su ciò che Google misura: il Largest Contentful Paint parte dal primo paint del contenuto, quindi la pagina risulta più lenta proprio dove prima era veloce. Con JavaScript disattivato resta un documento vuoto.
Una variante più subdola è mettere suppressHydrationWarning sull'elemento <html> e lasciare che sia il client a correggere la classe: l'avviso sparisce dai log e il secondo cambio di tema resta, perché la correzione avviene dopo l'idratazione. Silenziare l'errore non cambia l'ordine dei paint.
Un limite da mettere nel conto
La variante con cookie funziona solo se l'HTML è generato per quella richiesta. Dietro una CDN con cache statica il primo visitatore riempie la cache con il suo tema e gli altri ricevono quello: la classe arriva dal server, quindi non c'è flash, ma è il tema sbagliato. Servono Vary: Cookie o una pagina resa al bordo; senza, conviene tornare allo script inline e accettarne il costo.
Come prevenirlo
- Un solo meccanismo: classe sull'elemento radice, non media query e classe insieme.
- Imposta sempre
color-scheme: scrollbar e controlli nativi seguono il tema. - Registra i primi frame con la cache vuota per verificare l'assenza del flash.
Dove continuare
Nel catalogo: temi, componenti e blocco hero, oltre a blocchi. Le guide: dark mode senza secondo design system e checklist accessibilità; il confronto: temi gratuiti e premium.
Domande frequenti
Perché l'effetto React non basta?
Gli effetti girano dopo il primo paint. Il browser ha già disegnato la pagina con il tema chiaro.
Lo script inline viola la CSP?
Sì, se la policy vieta gli script inline. In quel caso serve un nonce o un hash: è un costo reale da mettere nel conto.
Meglio cookie o localStorage?
Con SSR il cookie è più semplice, perché il server può scrivere la classe giusta nell'HTML. Con SPA puro basta localStorage.
Cosa fa `color-scheme`?
Indica al browser quali colori usare per controlli nativi, scrollbar e sfondo di default. Senza, i controlli restano chiari anche in dark mode.
Come testo il flash?
Registra un video a 60 fps con la cache svuotata e guarda i primi frame. Un test automatico può confrontare il colore di sfondo prima dell'idratazione.