ProCust
Dark mode implementation: CSS variables, system settings, design

Dark mode implementation: CSS variables, system settings, design

How to build dark mode properly: colour tokens as CSS variables, prefers-color-scheme, a toggle without flashing, contrast, images and common mistakes.

In short: Good dark mode starts with semantic colour tokens as CSS variables that take different values per mode. By default the site follows the operating system through prefers-color-scheme, and a toggle with light, dark and system options overrides it. To avoid a flash on load, the theme has to be set before the browser paints. On the design side, a dark theme is a palette of its own with adjusted contrast, and an inverted light theme will not get you there.

Why dark mode is more than swapping colours

Many people set their operating system to dark and expect websites and web apps to follow. Anyone who opens a glaring white page at night notices the difference immediately. For applications people use for hours, like dashboards or internal tools, a dark theme is often a real comfort gain.

The effort depends almost entirely on how colours are organised in the code. If hex values are scattered across hundreds of components, dark mode becomes a major project. With a central colour system, it is manageable. That is one reason a design system pays off early.

The technical foundation: colour tokens as CSS variables

Name colours by role

Name colours by what they do, not by how they look. Instead of --grey-100, use --color-bg, --color-surface, --color-text, --color-text-muted, --color-border and --color-accent. Components only use these tokens. For dark mode you then change the token values in one place.

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #121417;
    --color-text: #e6e6e6;
  }
}
:root[data-theme="dark"] {
  --color-bg: #121417;
  --color-text: #e6e6e6;
}

The color-scheme property makes native elements such as scrollbars, form fields and date pickers render dark as well. Modern browsers also support the CSS light-dark() function, which lets you write both values side by side.

System preference and a manual toggle

A three-state toggle works well: light, dark and system. Store the choice in localStorage and set it as an attribute on the html element. Without a stored choice, the system setting applies. If JavaScript needs to react, for example to redraw charts, listen for changes with matchMedia('(prefers-color-scheme: dark)').

Prevent the flash on load

The most common bug is a brief white flash before the dark theme kicks in. It happens when the theme is only applied after the JavaScript bundle loads. The fix is a small inline script in the head that reads the stored choice and sets the attribute before the browser paints. In server-rendered React or Next.js apps you can also store the choice in a cookie and send it straight in the HTML.

Design: a palette of its own

Avoid pure black

Pure black with pure white creates harsh contrast that can shimmer when reading. Dark greys for backgrounds and slightly muted white for text are the norm. Highly saturated accent colours look loud on dark backgrounds and usually need lighter, less saturated variants.

Show depth with lightness, not shadows

Shadows barely show on dark backgrounds. Layers such as cards, dialogs and menus are better distinguished by slightly lighter surfaces: the higher an element sits, the lighter its surface.

Check contrast

The WCAG contrast requirements apply to both modes: at least 4.5:1 for body text and 3:1 for large text and UI controls. Check every token pair in both themes, not just the home page. Our article on web accessibility covers this in more detail.

Common pitfalls

Logos and graphics with transparent backgrounds and dark lettering disappear on dark surfaces. Separate variants via the picture element with media="(prefers-color-scheme: dark)", or SVGs that use currentColor, solve this. Photos can be dimmed slightly with a brightness filter.

Charts need their own colour series, because the light theme's colours read differently on a dark background. Our piece on the psychology of colour in digital products explains how strongly colour shapes perception. Also remember email templates, PDFs and the theme-color meta tag for the browser bar on mobile.

Frequently asked questions (FAQ)

Is dark mode worth it for every website?

For applications used for long stretches and products with a technical audience, almost always. For a simple company website it is more a question of polish. With a clean colour system the extra effort is small.

Can dark mode be added later?

Yes. The first step is replacing hard-coded colours with tokens. That is tedious, but it also makes the light theme easier to maintain. After that, the dark theme is mostly design work.

Does dark mode save battery?

On OLED displays, dark pixels emit less light and use less power. On LCD screens the effect is practically zero. Battery life is therefore only a real argument for mobile use.

Should dark mode be the default?

The best default is to follow the user's system setting. Everyone gets what they already prefer and can change it with the toggle.

Conclusion

Good dark mode is half architecture and half design. If you set up colours as semantic tokens from the start, the dark theme comes almost for free, and adding further themes or brand variants later becomes easy too.

If you are planning a web app with well-designed theming, talk to us about web development.