WhollySoftware
Back to blogWeb

Dark Mode Implementation That Respects System Preferences and User Overrides

Wholly Software TeamNovember 3, 20257 min read
Dark Mode Implementation That Respects System Preferences and User Overrides

A media client's dark mode implementation had a visible flash of the light theme on every page load before switching to dark, which is a small thing but reads as broken to users and was generating support messages calling it a bug. The cause was standard for client-rendered theme logic: the theme preference was read from localStorage in a useEffect after the initial render, so the page always painted in the default light theme first, then flickered to dark once React hydrated and the effect ran.

The fix was moving theme detection out of React entirely for the first paint. We added a small inline script in the document head, before any stylesheet or app JavaScript loads, that reads the stored preference (or falls back to the prefers-color-scheme media query) and sets a data-theme attribute on the html element synchronously. CSS custom properties keyed off that attribute mean the correct theme is applied before the browser paints anything, eliminating the flash entirely.

The preference hierarchy needed to be explicit: an explicit user choice (stored after they click the toggle) always wins, falling back to system preference (via prefers-color-scheme) if the user hasn't set an explicit preference, falling back to light as the final default. We track these as three distinct states — 'light', 'dark', and 'system' — rather than a boolean, since collapsing 'system' into whatever it currently resolves to loses the ability to follow the OS if the user changes it later.

We listen for changes to the prefers-color-scheme media query only when the user's stored preference is 'system,' updating the theme live if they switch their OS theme mid-session — useful for anyone with an automatic sunset/sunrise theme switch on their OS. If the user has an explicit override set, we don't listen to that media query at all, so their choice doesn't get silently reverted by an OS-level change.

The last detail that mattered: components with images, especially charts and screenshots in documentation, needed theme-aware variants rather than one image that looked wrong in the other mode. We used the picture element with a media query on the source tag tied to the same data-theme attribute logic, so illustrations swap along with the rest of the UI instead of a light-mode chart sitting awkwardly on a dark background.

Since shipping this, the flash-related support messages stopped entirely, and analytics showed about 38% of users actively chose an explicit theme rather than following system default, which was higher adoption of the toggle than the client expected and validated keeping it prominent in the header rather than buried in settings.

Dark ModeCSSFrontendUX
Dark Mode Implementation That Respects System Preferences and User Overrides — Wholly Software