# Dark mode without the flash · Components

<!-- https://learn-ui.com/chapters/components/dark-mode -->

A theme switch is two lines of CSS and a long tail of things that go wrong: a flash of light before dark, a choice that ignores the system, colours animating at different speeds, a tab icon that stops matching. Paco Coursey’s **next-themes** (An abstraction for themes in your React app. (next-themes README)) handles that tail; its [README](https://github.com/pacocoursey/next-themes) lists the problems as features: “Perfect dark mode in 2 lines of code,” prefers-color-scheme, “No flash on load (both SSR and SSG),” sync across tabs, forced themes, and a useTheme hook.

### Two lines and the defaults

“Adding dark mode support takes 2 lines of code”: wrap the app in ThemeProvider, a client component. By default it sets data-theme on the html element.

> Figure. app/layout.jsxPaco Coursey, next-themes README

The defaults say what a theme should do when you say nothing. defaultTheme is ‘system’ (earlier versions: light); enableSystem is true, and with it off the default is light; enableColorScheme is true, so native inputs follow; the attribute is ‘data-theme’, or class, or any data-\*; storageKey is ‘theme’ in localStorage; themes is \[‘light’, ‘dark’\], overridden by your own list, since the library is “completely CSS independent”; disableTransitionOnChange is false.

forcedTheme pins a page without touching the saved setting, and then you should disable your switching UI. useTheme returns theme, setTheme, resolvedTheme (what ‘system’ resolved to), systemTheme, and themes with ‘system’ appended.

### Known before first paint

No flash means a script injected before paint, with two caveats. The server cannot know the theme, so useTheme values are undefined until the client mounts, and rendering from them earlier is a hydration mismatch; the README suggests a skeleton until mounted, and suppressHydrationWarning on html because next-themes edits that element. And Cloudflare’s Rocket Loader breaks the injected script, hence scriptProps and nonce.

### Switch without transitions

disableTransitionOnChange links to Paco’s 2020 post [Disable transitions on theme toggle](https://paco.me/writing/disable-theme-transitions). Against the instinct to transition every colour: “Adding a CSS transition to every element negatively impacts rendering performance, and it also won’t work for images, icons, and CSS properties that don’t support transitions,” and differing durations would feel inconsistent.

So he injects a stylesheet setting transition: none !important on everything, with manual browser prefixes since the CSS is not preprocessed, toggles the theme, forces a repaint, and removes it. requestAnimationFrame proved unreliable; getComputedStyle works on every major browser “because it forcibly applies all active stylesheets.” He credits Guillermo for the idea.

> Figure. JavaScriptPaco Coursey, Disable transitions on theme toggle

### The favicon and the temperament

Dark mode reaches into the browser chrome. In 2023 Paco wrote [Safari favicon showing white background on dark mode](https://paco.me/writing/safari-favicon-showing-white-background): as recently as Safari 16.4, a favicon failing Safari’s contrast requirements sits on a white background in dark mode. He could not pin the requirements down; AA 4.5:1 or AAA 7:1 against both #000 and #282828, Safari’s default dark tab bar, gave no consistent behaviour.

The fix is a brighter primary colour, less white space, or a border. For Linear he changed #6E79D6 to #8299FF.

That these posts are short and specific is not an accident. In [Redesign 2021](https://paco.me/writing/redesign-2021) Paco strips his site to documents and links: “this iteration reflects my values of performance, simplicity, and craft.” It runs Next.js with JavaScript disabled, because “you don’t need it to read documents.” The same temperament produces a library whose pitch is two lines and stop thinking about it.

Try it

This site runs on next-themes with the class attribute, system as the default, and disableTransitionOnChange set. Press D anywhere outside a text field to flip the theme and notice that nothing fades.

A theme switch is not a colour change; it is a state that must be known before first paint, follow the system, persist and sync, not animate element by element, and reach the favicon. Each is a small decision with a documented default, and the person who wrote them down chose, every time, the quiet option.
