Dark Mode Colour
Dark mode is not an inversion of light mode — it requires a new understanding of surface, contrast, and vibrancy for a fundamentally different visual environment.
The naive approach to dark mode is CSS inversion: swap white backgrounds for black, swap dark text for white, call it done. The result is ugly. Pure black backgrounds create harsh contrast and show display artifacts on OLED screens. Fully saturated accents become blinding at low ambient light. The same accent colour that reads as confident orange in light mode reads as garish against a dark surface. Dark mode requires its own palette, not an inverted one.
Dark mode ≠ inverted light mode
The fundamental insight is that dark mode creates a different luminance environment. In light mode, the dominant field is bright — the background is near-white, surfaces emit a lot of light, and colour appears against a reflective backdrop. In dark mode, the dominant field is dark — backgrounds emit minimal light, and elements appear as light sources emerging from darkness.
This changes the behaviour of every colour value. A vivid blue that reads well at saturation 80% on a white background may need to be dialed back to 60% saturation in dark mode to avoid appearing luminous and overwhelming against a dark surface. Conversely, text that was dark-on-light in light mode needs to lighten in dark mode — but not all the way to pure white, which creates excessive contrast.
Surface levels in dark UIs
Light UIs use shadows to create elevation: a card sits above a background, a modal sits above a page. In dark UIs, shadows are nearly invisible against dark surfaces — elevation is instead communicated through lightness. Surfaces closer to the viewer are slightly lighter; surfaces further away are slightly darker. This is the opposite convention from light mode, where higher elevation means a lighter shadow, not a lighter surface.
A practical surface system for dark UIs uses 3–4 lightness levels. Background: near-black (#0f172a). Surface 1 (cards, panels): slightly lighter (#1e293b). Surface 2 (modals, dropdowns): lighter still (#334155). Interactive hover: slightly lighter than the base surface. Each step should be perceptibly different but not jarring — a 5–10% lightness increase per level is typical.
Saturation in dark backgrounds
Vivid colours become more intense against dark backgrounds — the same HSL saturation value reads as more vivid when the surrounding field is dark. This means accent colours need to be dialed back, not just lightened.
The common correction is to shift the accent to a lighter, slightly less saturated version in dark mode. Our site’s amber accent is amber-700 in light mode and amber-400 in dark mode. Same hue, different value: lighter value for contrast against dark backgrounds, slightly lower saturation to avoid becoming overwhelming. This adjustment needs to happen for every semantic colour in your system — success greens, warning ambers, error reds, info blues all need dark-mode variants that are lighter and sometimes less saturated.
Maintaining brand in dark mode
Dark mode can feel like a completely different product if the brand character doesn’t transfer. Two things maintain continuity. First, preserve the hue of your brand colour — users recognise the hue even when the lightness and saturation shift. Second, preserve the temperature of your palette. If your light mode has a cool, clinical character, your dark mode should too — dark navies and cool near-blacks rather than warm near-blacks. If your light mode is warm, use warm dark surfaces.
The areas where dark mode diverges from light mode — surface lightness, accent values, text lightness — are structural necessities. The areas where it should maintain consistency are tonal and hue-based. Keep the same relationship between your accent and your neutrals, even as the specific values change. Build your colour system with separate light and dark semantic tokens that point to different values in the base scale — this makes dark mode a configuration rather than a separate design.
Implementing dark mode
The CSS prefers-color-scheme media query detects the user’s OS-level preference:
@media (prefers-color-scheme: dark) {
:root {
--colour-bg: var(--slate-900);
--colour-text: var(--slate-100);
--colour-accent: var(--amber-400);
}
}
Most products also offer a manual toggle inside the app, allowing users to override the OS setting. When both exist, the product-level preference must take priority over the media query — the user has expressed a more specific preference that should not be overridden by the OS setting.
The standard approach uses a class on the root element (<html class="dark">) for the manual override. The semantic tokens in c15 point to dark-mode values when this class is present. On page load, a small script in <head> reads the persisted user preference and applies the class before any paint, preventing a flash of the wrong mode:
const saved = localStorage.getItem('colour-scheme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.classList.toggle('dark', (saved ?? system) === 'dark');
The product toggle updates localStorage and toggles the class in real time. The media query serves as the default when no preference has been saved — removing the need to specify a default explicitly in JavaScript.