← Back to all posts
Engineering

A Themeable Dark Mode with Just CSS Variables

No framework required — a few dozen lines of CSS variables give you an elegant, flicker-free dark mode.

Dark mode shouldn’t need a heavyweight theming library. CSS custom properties (variables) can carry almost all of the weight. This post takes apart the exact approach this blog uses.

Every color comes from a variable

Step one: pull every color into a variable, defined centrally on :root:

:root {
  --bg: #ffffff;
  --text: #0d0f14;
  --accent: #2f6bff;
  --border: #e9ebf0;
}

[data-theme='dark'] {
  --bg: #0b0d12;
  --text: #f2f4f8;
  --accent: #5b86ff;
  --border: #222632;
}

From then on every rule references a variable and never hard-codes a color:

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

Switching themes is just flipping one data-theme attribute on <html> — every color across the site updates at once.

The handful of lines of JS

const next =
  document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);

localStorage remembers the choice so it persists on the next visit.

Killing the flash

The most overlooked trap: the page loads light, then snaps to dark. The cause is the theme script running too late.

The fix — inline that script at the very top of <head> so it runs before the first paint:

<script is:inline>
  const saved = localStorage.getItem('theme');
  const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.dataset.theme =
    saved || (prefersDark ? 'dark' : 'light');
</script>

The key phrase is “before paint.” Even one frame late, and the user sees that jarring flash.

Two small touches

  • Use color-mix() to derive translucent backgrounds from your accent color, so you don’t define yet another batch of variables.
  • Disable transitions for users with prefers-reduced-motion to respect anyone sensitive to motion.

A few dozen lines of CSS, zero dependencies, buttery theme switching. Sometimes the platform’s native abilities are far stronger than we assume.