Dark Mode with CSS Variables and light-dark()
Good dark mode isn't just inverted colours. It needs its own palette, respects the user's system setting, and lets them override it.
1. Use semantic colour variables
:root {
--bg: #ffffff;
--surface: #f8fafc;
--text: #0f172a;
--muted: #64748b;
--brand: #0d9488;
}
body { background: var(--bg); color: var(--text); }
Components use --surface and --text, never raw colours, so switching themes only means changing the variables.
2. Follow the system setting
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b1120;
--surface: #111827;
--text: #e2e8f0;
--muted: #94a3b8;
--brand: #2dd4bf;
}
}
3. Or use light-dark()
:root {
color-scheme: light dark; /* also styles scrollbars and form controls */
--bg: light-dark(#ffffff, #0b1120);
--text: light-dark(#0f172a, #e2e8f0);
}
light-dark() picks the first value in light mode and the second in dark mode, based on the element's color-scheme.
4. Add a toggle that remembers
const saved = localStorage.getItem('theme'); // 'light' | 'dark' | null
if (saved) document.documentElement.style.colorScheme = saved;
toggle.addEventListener('click', () => {
const systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
const current = document.documentElement.style.colorScheme || (systemDark ? 'dark' : 'light');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.style.colorScheme = next;
localStorage.setItem('theme', next);
});
Setting color-scheme on the root element switches every light-dark() value at once. Run the "read saved theme" part in a small inline script in the <head> so the page doesn't flash the wrong theme on load.
Dark palette tips
- Avoid pure black backgrounds; very dark blue-grey is easier on the eyes.
- Lower saturation of bright brand colours slightly.
- Show elevation with lighter surfaces instead of shadows.
- Re-check text contrast; it's easy to go too dim.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.