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