# Dark mode without a flash: settle the theme before the first paint

The white flash comes from applying the theme after React mounts. Decide the class before paint, with a blocking script or a cookie read on the server.

> Page: https://ui.eneawork.it/blog/dark-mode-senza-flash-tema
> Markdown: https://ui.eneawork.it/blog/dark-mode-senza-flash-tema.md

*2026-11-01*

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.

La domanda «How do I detect dark mode using JavaScript?» vale 430 voti su Stack Overflow (letto il 31 ottobre 2026 con la Stack Exchange API): fonte. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, verificati su /api/stats il 1 novembre 2026. Ogni comando qui sotto è eseguibile in locale: nessun passaggio richiede un account.

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

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

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

## FAQ

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