This site's own dark theme is the example I keep coming back to when explaining this to clients, mostly because building it surfaced every mistake described below before I caught it. Dark mode done well is closer to print design — adjusting contrast and weight deliberately — than to a CSS filter you apply once and forget.
Why "just invert it" doesn't work
The naive approach — a global filter: invert(1) or swapping every color for its complement — breaks two things immediately: photographs invert into something unusable, and colors that worked at one lightness value stop working at the inverted one. A blue link that was readable on white often becomes nearly invisible on near-black, because the contrast ratio that mattered was specific to the original pairing.
Elevation through lightness, not shadow
In a light UI, elevation — the sense that a card floats above the page — comes from shadow, because shadows are visible against a light background. On a dark background, shadows barely register. The fix used throughout most well-built dark themes, including this one, is elevation through lightness instead: each layer gets slightly lighter than the one beneath it.
This site's elevation scale
--bg: #0D1117; /* page background */
--bg-raised: #141A21; /* footer, raised sections */
--bg-card: #161B22; /* cards, code blocks */
Each step up is a card "closer" to the viewer, signaled by being a few percent lighter rather than by a shadow that wouldn't read against the dark backdrop anyway.
Text contrast — and where pure white is wrong
Pure white text (#FFFFFF) on a near-black background produces a contrast ratio so high it causes a halation effect — text that seems to glow or vibrate slightly, especially for readers with astigmatism. Most well-executed dark themes use an off-white for body text and reserve true white for headings and high-emphasis elements only.
This site uses #C9D1D9 for body copy against the #0D1117 background, a ratio comfortably above WCAG AA for body text but without the glare of pure white. Headings step up to near-white for emphasis, since short, large text tolerates higher contrast better than dense paragraphs do.
Run your actual color pairs through a contrast checker rather than trusting your monitor's calibration. A combination that looks fine on a bright desktop display can fail WCAG AA, and will look noticeably worse on a dimmer or differently-calibrated screen.
What happens to brand color
A saturated brand color that works on white frequently looks muddy or low-contrast on near-black. The fix is rarely the same hue at the same saturation — it's usually a slightly desaturated, slightly lightened version specifically tuned for the dark background, treated as a deliberate variant rather than the same token reused everywhere.
For this site, the ice-accent color sits at a lightness that reads clearly against #0D1117 without becoming neon or strobe-like in a dim room, which a more saturated cyan would risk doing.
Building it with tokens, not duplicate stylesheets
The maintainable way to support both themes is CSS custom properties scoped to a root attribute, swapped with a single class or data attribute rather than maintaining two parallel stylesheets that inevitably drift out of sync:
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #0D1117;
--text: #C9D1D9;
}
Every component references the variable, never a literal color, so adding or adjusting a theme is a change in one place rather than a hunt through every component file. It's a small discipline that pays for itself the first time a client asks for a third theme variant.