EMZETT.
Login

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-theme on <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 (suppressHydrationWarning on html).
  • 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 #12141a and #e8eaf0.
  • Lighten accent colours and lower saturation.
  • Elevation through lighter surfaces instead of shadows.
  • Images/logos: dark variants, filter/mix-blend-mode, SVG with currentColor.
  • 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