Inverting a light theme produces a dark theme that is technically dark and genuinely unpleasant. The colours that worked on white are too saturated on black, the shadows that conveyed depth are invisible, and pure white text on pure black causes visible halation for many readers.

Dark mode is a separate design, not a transformation. Here's what actually changes.


Don't use pure black, don't use pure white

Ground: a very dark neutral rather than #000000. Something in the range of #12100c to #1a1a1a. Pure black creates maximum contrast with light text, which sounds good and reads badly — the effect is halation, where light text appears to bleed into the background. It's fatiguing over long sessions and worse for readers with astigmatism.

Pure black also removes your ability to show depth, because there is nothing darker to recede to.

Text: an off-white rather than #FFFFFF. Something like #efe8db or #e8e8e8. Slightly warm off-whites are noticeably more comfortable than clinical greys against a dark ground.

The pairing to aim for is high contrast without being maximal — comfortably readable, not glaring.


Elevation is lightness, not shadow

On a light interface, raised surfaces are conveyed with shadow. On a dark interface, shadows are invisible — you cannot cast a dark shadow on a dark background.

Dark interfaces convey elevation by getting lighter. Higher surfaces are lighter than the ground.

css
:root {
  --ground:     #12100c;   /* page background        */
  --surface-1:  #191611;   /* cards, panels          */
  --surface-2:  #221e17;   /* modals, popovers, menus */
  --border:     #2a251d;   /* dividers               */
}

Three or four levels is enough. More becomes indistinguishable and gives people decisions they can't make consistently.

Keep the steps small — a large jump reads as a different colour rather than a raised surface. And keep the ordering strict: a modal over a card must be lighter than the card.


Desaturate your accent colours

This is the step most often skipped, and it's why converted themes look garish.

A saturated brand colour that looks confident on white becomes aggressive on near-black. The perceived intensity of a colour rises substantially against a dark ground.

Reduce saturation and raise lightness for the dark variant. A vivid mid-blue that works on white typically needs to become a lighter, softer blue to read comfortably on dark.

This means your accent colours are different values between themes, not the same value used twice — which is exactly the argument for semantic tokens over primitive ones:

css
:root {
  --color-action: #075bc0;         /* light theme */
}

@media (prefers-color-scheme: dark) {
  :root { --color-action: #56c6e6; }   /* dark theme — lighter, softer */
}

Contrast requirements, precisely

WCAG 2.1 AA, which is the practical standard:

ElementMinimum ratio
Body text4.5:1
Large text (18pt+, or 14pt+ bold)3:1
UI components and graphical objects3:1
Focus indicators3:1 against adjacent colours

Points that are commonly missed:

Placeholder text usually fails. The typical light grey placeholder rarely meets 4.5:1. If it conveys necessary information, it must pass — and if it doesn't convey necessary information, consider whether it should exist.

Disabled states are exempt from the requirement, but if users need to read them, hold them to it anyway.

Borders and icons need 3:1. A subtle divider that's decorative can be lower; one that conveys structure cannot.

Focus rings need 3:1 against what's adjacent — which on a dark theme often means a lighter ring than you'd use on white.

Contrast is a pair, not a colour. Check every text-on-surface combination you actually ship, including text on --surface-2, not just text on the ground.


Implementation

Support system preference and an explicit override, and make sure the override wins in both directions:

css
:root {
  --ground: #efe8db;
  --text: #12100c;
}

@media (prefers-color-scheme: dark) {
  :root { --ground: #12100c; --text: #efe8db; }
}

/* An explicit user choice must beat the system preference either way. */
:root[data-theme="dark"]  { --ground: #12100c; --text: #efe8db; }
:root[data-theme="light"] { --ground: #efe8db; --text: #12100c; }

The two attribute selectors matter. With only the media query and a dark class, a user on a dark-preferring system cannot force light mode.

Set color-scheme so native controls, scrollbars, and form elements follow:

css
:root { color-scheme: light dark; }

Without it you get light scrollbars and white-flashing native inputs on a dark page.

Avoid the flash of wrong theme. If the user has chosen explicitly, apply the attribute in a small blocking script in <head>, before paint. A theme that flickers on every navigation reads as broken.


Details that get missed

Images and illustrations. Photos with white backgrounds punch a hole in a dark layout. Logos and diagrams need dark-mode variants. Consider <picture> with a prefers-color-scheme source.

Semantic colours. Success green and error red both need dark-theme variants. The default red on near-black is usually too intense.

Code blocks. Syntax themes designed for light backgrounds often fail contrast on dark. Use a theme built for it.

Charts. Categorical palettes tuned for white grounds frequently lose distinguishability on dark. Verify each series is distinguishable both from the ground and from each other.

Test in a dark room. That's when it will be used. A dark theme evaluated on a bright monitor in daylight will be too dim in the conditions people actually reach for it.


Want interfaces built properly?

We design and build interfaces with dark mode, contrast, and focus states handled as part of the system rather than retrofitted. Talk to us about graphic design and branding.