Dark mode without the flash

There’s a particular kind of dark-mode implementation that I find genuinely annoying: the one that loads in light mode, paints the page, and then snaps to dark a fraction of a second later. The cure is small and worth writing down.

The trick

The flash happens because the JavaScript that reads the user’s preference runs after the first paint. The fix is to do that work in a tiny inline script in the document <head>, before the body is rendered.

<script>
  (function () {
    var stored = localStorage.getItem("theme");
    var prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
    var theme = stored || (prefersDark ? "dark" : "light");
    document.documentElement.dataset.theme = theme;
    document.documentElement.style.colorScheme = theme;
  })();
</script>

Two things matter here:

  • It’s synchronous and inline, so it runs before paint.
  • It writes to data-theme on <html>, which I use as the hook for all my CSS custom property overrides.

CSS side

The CSS is just a token swap:

:root {
  --bg: #fbf8f3;
  --fg: #1f1b16;
}

:root[data-theme="dark"] {
  --bg: #0e0d0b;
  --fg: #f4f1ea;
}

body {
  background: var(--bg);
  color: var(--fg);
}

That’s the whole thing. No framework, no flicker, no third-party script.