Implementing dark mode and theming with CSS variables
In short: With CSS variables for colours, the prefers-color-scheme media query and a toggle you can build a clean dark mode that respects the system setting and saves the choice.
In more detail: A good dark mode is more than inverting colours: sufficient contrast, muted surfaces, adjusted shadows and images. With design tokens (named colour variables) everything stays maintainable.
In Depth
Variables as design tokens
:root {
--bg: #ffffff;
--text: #1a1a1a;
--muted: #5c6370;
--accent: #0b6bcb;
--border: #d9dde3;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #12141a; --text: #e8eaf0; --muted: #9aa3b2;
--accent: #6ea8ff; --border: #2a2f3a;
}
}
:root[data-theme="dark"] { --bg: #12141a; --text: #e8eaf0; /* … */ }
body { background: var(--bg); color: var(--text); }Use semantic names (--bg, --text, --accent), not --blue. color-scheme: light dark colours form elements, scrollbars accordingly. New: light-dark() function and @property.
System, toggle, saving
- Follow the system via
prefers-color-scheme(default). - Toggle light / dark / system: sets
data-themeon<html>; remember the choice in localStorage (in try/catch). - Avoid flicker (FOUC): a small inline script in
<head>that sets the preference before first render. - In Next.js: a library such as next-themes or your own provider; watch for hydration differences (
suppressHydrationWarningonhtml). - Add
<meta name="theme-color">for the browser bar.
Design in the dark
- Check contrast per WCAG (text ≥ 4.5:1), not pure black/white (#000/#fff dazzles): rather
#12141aand#e8eaf0. - Lighten accent colours and lower saturation.
- Elevation through lighter surfaces instead of shadows.
- Images/logos: dark variants,
filter/mix-blend-mode, SVG withcurrentColor. - Charts and code highlighting with their own palettes.
- Test transparent PNGs on dark.
- Retro/gaming styles are their own themes: same variable structure, different values.
Testing and accessibility
In DevTools Rendering → emulate prefers-color-scheme; test on real devices (OLED, sunlight). Focus rings and error colours visible in both modes. Also respect forced-colors/high contrast (@media (forced-colors: active)) and prefers-contrast. Keep the print style light. Never use colour alone as information (add pattern/text). Document tokens in a small style guide.
See also: Accessible websites