The fastest way to a bad dark mode is to invert the light one. Swap white for black, black for white, ship it, and discover that the brand color now vibrates, the shadows have disappeared, and everything looks slightly cheap. Dark mode is a second palette, related to the first but built by different rules.
Here is what actually changes.
Do not use pure black
Pure black backgrounds cause two real problems.
The first is halation. White text on pure black appears to bleed at the edges for many readers, and noticeably so for anyone with astigmatism — which is a large minority. Letterforms smear, and long-form reading becomes tiring. The pairing scores 21:1 on contrast, the theoretical maximum, and is still worse to read than a softer combination.
The second is that pure black leaves you nowhere to go. Interfaces convey depth through surfaces sitting above other surfaces — a card on a page, a menu above the card, a dialog above that. In light mode this is done with shadows. In dark mode shadows barely register, so elevation is conveyed by making higher surfaces lighter. Start at #000000 and your base layer has no room beneath it.
Start instead around #0B0F14 to #141922, and build a ladder of surfaces upward from there.
Elevation replaces shadow
A workable structure is four or five steps:
- Base — the page background, your darkest value.
- Surface — cards and panels, a little lighter.
- Raised — hover states, dropdowns, popovers.
- Overlay — modals and dialogs, lightest of the set.
Keep the steps small — a few percent of lightness each. Large jumps look like mistakes rather than depth. The tint generator produces this kind of ramp from your base color in one pass, and using tints of the base rather than neutral gray keeps the whole stack coherent.
Shadows still have a role, but a different one: a subtle dark shadow under a raised surface reinforces the lighter fill without being the main signal. And a hairline border, one step lighter than the surface it sits on, often does more work than any shadow.
Saturated colors need to come down
This is the change most inverted palettes miss. A saturated color that reads well on white is frequently painful on near-black. The high contrast between vivid hue and dark ground produces a shimmering effect at edges, and saturated blues in particular can look like they are glowing.
The fix is counterintuitive if you are thinking about contrast alone: reduce saturation and increase lightness. A brand blue at hsl(217 91% 60%) might become roughly hsl(217 70% 68%) for dark mode — recognisably the same color, considerably easier to look at.
Our tone generator produces desaturated variants directly, and the RGB to HSL converter is the quickest way to see the numbers you are adjusting.
The exception is small elements. A tiny badge or a thin icon can carry more saturation than a large fill, because the effect scales with area. A large panel in your accent color will almost always need muting; a 16px dot usually will not.
Text: not white, and not one value
Use an off-white rather than #FFFFFF — something in the region of #E7ECF2. Against a dark surface this still lands well above the 4.5:1 requirement while avoiding the harshness of maximum contrast.
Then build a hierarchy, because everything cannot be equally loud:
- Primary text — the off-white, for body copy and headings.
- Secondary text — captions, metadata, helper text. Dimmer, but still clearing 4.5:1 against its background.
- Disabled — the only tier permitted to fail contrast, because it is signalling non-interactivity.
A word of caution about achieving these tiers with opacity. Semi-transparent white over varying surfaces produces a different effective color on each one, so a value you verified on the base surface may fail on a raised card. Opacity is convenient; explicit color values are predictable. If you use opacity, check the result on every surface it can land on.
Contrast has to be rechecked from scratch
Contrast is a relationship between two colors. Change both and every prior result is void.
The failures tend to be systematic rather than random. Mid-tone grays that had comfortable contrast against white often sit awkwardly against a dark surface — too close to the background to read, too light to feel like a background themselves. Borders that were a subtle light gray disappear completely. Focus rings tuned for a white page can be nearly invisible.
Work through every pair again in the contrast checker: body text, secondary text, link color, border against surface, focus ring against every surface it can appear on, icon against fill. The accessible color guide covers the thresholds including the 3:1 requirement for UI components, which is the one most often missed in dark themes.
Images and illustrations need attention too
Content usually survives the switch; assets often do not.
Logos with dark text become unreadable — most brands maintain a light variant for exactly this reason, and if yours does not, this is the moment to make one. Illustrations with white backgrounds appear as glaring rectangles. Screenshots of light interfaces sit oddly in a dark page; a subtle border helps them read as embedded rather than pasted.
Charts and data visualisation need their own pass. A palette tuned for a white plot area rarely works on dark — see our post on color in data visualization for how to rebuild one properly.
Implementation: one variable set, two values
The clean way to build this is CSS custom properties, defining the palette twice and letting the cascade do the rest:
:root {
--surface: #FFFFFF;
--surface-raised: #F7F9FC;
--text: #0B0F14;
--text-muted: #56606D;
--brand: hsl(217 91% 60%);
}
[data-theme="dark"] {
--surface: #0B0F14;
--surface-raised: #141922;
--text: #E7ECF2;
--text-muted: #9AA6B4;
--brand: hsl(217 70% 68%);
}
No component CSS changes. The CSS color generator produces variable blocks in this shape, and our CSS color guide covers the naming decisions that make a system like this survive a redesign.
Two implementation details worth getting right. Respect prefers-color-scheme as the default so the first paint matches the user's system setting, while still offering an explicit toggle — plenty of people want a different choice on a specific site. And apply the theme before first paint, usually with a tiny inline script, or users get a white flash on every navigation.
A checklist
- Base surface is near-black, not pure black.
- Four or five surface steps exist for elevation.
- Saturated brand colors have been muted and lightened.
- Body text is off-white, with a defined muted tier.
- Every contrast pair has been re-checked in dark mode.
- Borders and focus rings are visible on every surface.
- Logos, illustrations and charts have dark-mode variants.
- The theme applies before first paint.
Dark mode done properly takes about the same effort as the original palette. Done as an inversion, it takes ten minutes and looks like it.