30/5/2026 · 13 min read
Dark mode went from a power-user novelty to a mainstream expectation in roughly three years. The operating systems flipped the switch, the apps followed, and suddenly every design team had a second theme to maintain — whether they were ready for it or not. Most were not. The result is a generation of dark modes that look like the light mode held at gunpoint: the same hierarchy, the same colours, just dimmed to near- black and shipped before anyone asked whether it actually worked.
The dirty secret is that bad dark mode is easy to ship and hard to diagnose. It passes a glance test — it is dark, it renders, users accept it — but it quietly erodes the visual quality of everything it touches. Depth disappears. Hierarchy flattens. Text becomes either glaring or unreadable, and often alternates between both on the same screen. This article is about understanding why that happens and building the mental model you need to design dark mode that actually works, not just dark mode that exists.
The first instinct every designer has when asked to add dark mode is to invert the palette. White backgrounds become near-black, dark text becomes near-white, and coloured accents get tweaked until they stop burning the eyes. It takes an afternoon, it looks roughly correct in a screenshot, and it is almost entirely wrong.
The first problem is grey. Light mode uses grey as a subordinate — a step down from white that signals secondary information, disabled states, or subtle dividers. Invert that grey and you get a mid-tone that sits uncomfortably between the background and the foreground, reading as neither. Light mode grey communicates hierarchy because it is darker than the background; inverted dark mode grey communicates nothing because it is lighter than the background by roughly the same margin — but the human visual system does not experience lightness relationships symmetrically. What reads as "slightly subdued" in light mode reads as "slightly glowing" in dark mode, and the effect is visual noise, not hierarchy.
The second problem is shadow. Light mode uses drop shadows to simulate depth — a surface casts a shadow on whatever is behind it, and the shadow confirms the elevation. Invert the palette and the background is now dark, which means a dark shadow on a dark background is invisible. Teams usually respond by lightening the shadow, which produces a soft glow that looks more like a light source than a depth cue, and undermines the very hierarchy it was meant to reinforce. Shadow as a depth signal is a light-mode-native idea. In dark mode, it needs to be replaced, not preserved.
The third problem is brand colour. Saturated colours that work beautifully on white frequently look aggressive, fluorescent, or simply wrong on near-black. The luminance relationship between colour and background is completely different. A blue that communicates "primary action, confident, trustworthy" on a white background can communicate "neon, alarming" on a dark one. Inversion treats brand colours as neutral facts; they are not. Every colour in your palette needs to be reconsidered against the dark surface it will actually sit on.
The correct architecture for dual-theme design is not a pair of colour
palettes. It is a single layer of semantic tokens that sit between your design decisions
and your raw colour values — tokens whose names describe intent rather than appearance.
Not gray-900 and gray-50, but surface-background,
surface-elevated, text-primary, text-secondary,
border-default. The token names stay constant across both themes; only
their resolved values change.
In practice this means defining your design token layer in CSS custom
properties. A :root block sets the light-mode defaults; a
[data-theme="dark"] or @media (prefers-color-scheme: dark)
block overrides the same variables with their dark-mode equivalents. Every component in
your design system references only the semantic tokens — never the raw hex values. When
you switch themes, the component does not need to change at all. The token resolves to
a different colour, and the component picks it up automatically.
The pay-off is not just cleanliness. The semantic token model forces you to make the design decision explicitly: what is this surface for, and what colour should that intent resolve to in each context? That discipline catches the inversions that look correct at a glance but break the hierarchy in ways you only notice after living with the interface for a week. It also makes maintenance tractable — when your brand refreshes its blue, you change one raw value in the palette layer, and every semantic token that references it updates automatically across both themes.
The mistake teams make most often is building semantic tokens for only the obvious cases — background, text, accent — and leaving everything else as hardcoded values. That works until the first edge case: a tooltip with its own background, a code block that needs a slightly different surface, a success state that uses a colour only expressible in the palette layer. Build the token vocabulary comprehensively from the start. The overhead is an extra hour in setup and a decade of sanity in maintenance.
Typography in dark mode is where the most teams stumble because the problems
are subtle enough to miss in a design review but obvious enough to notice in extended
use. The most common mistake is pure white text on a pure black background. It passes
WCAG contrast checks with flying colours — a contrast ratio of 21:1, the theoretical
maximum — and it is genuinely uncomfortable to read for more than a few sentences. The
issue is halation: the bright white text appears to bleed into the dark background at
the edges of each letterform, making the text look slightly blurry even when it is
rendered at full sharpness. The fix is simple: use off-white, not white. Something in
the range of #E8E8E8 to #F0F0F0 — luminance around 80–85%
rather than 100% — eliminates halation while keeping the contrast ratio well above
accessible thresholds.
Font weight is the second variable almost nobody adjusts. The same typeface at the same weight reads differently on dark and light backgrounds because of how the human visual system processes luminance contrast. On a light background a regular weight is crisp and appropriately substantial. On a dark background the same weight can read as thin and slightly ghostly, particularly at smaller sizes. The correction is to increase font weight by one step for body text in dark mode — regular becomes medium, medium becomes semibold — and to test the result on the actual device, not in a design tool preview that renders differently from a browser.
Secondary text is the third problem. In light mode, secondary text is typically achieved with reduced opacity or a lighter grey — the visual distance from primary text is easy to read and comfortably subtle. In dark mode, secondary text needs to be handled with precision. Too close to the background and it disappears; too close to primary text and it loses its secondary character. I use a three-step luminance scale: primary text at ~85% white, secondary text at ~55% white, and disabled text at ~30% white. Those numbers will shift depending on your background tone, but the principle — explicit luminance steps rather than opacity shortcuts — produces more predictable results across different rendering environments.
In light mode, elevation is expressed through shadow. A card above the page surface casts a shadow downward; the shadow confirms the card is above the background. A modal casts a deeper shadow; the deeper shadow confirms it is above the card. The system is intuitive because it mimics physical light hitting physical surfaces — we have been reading it since childhood.
Dark mode breaks the shadow model because there is no plausible dark-room light source that would cast shadows visible against a near-black background. The solution, used consistently in Material Design 3 and increasingly everywhere else, is to express elevation through surface lightness rather than shadow darkness. Higher- elevation surfaces are lighter, not higher-shadowed. A base background sits at the darkest point in the scale; a card sits one step lighter; a modal sits two steps lighter; a tooltip or popover sits at the top of the scale. The tint progression replaces the shadow progression.
The tint values need to be subtle. The goal is a perceptible step, not a
dramatic one — on a background of #121212, a card surface at
#1E1E1E reads as elevated without announcing itself. Each step up should
add roughly 5–8 luminance points; beyond that the surfaces start to look like distinct
panels rather than an elevation stack. For products that need to communicate more than
three or four elevation levels, you can combine the tint system with a very subtle
border at the top edge of elevated surfaces — a 1px line at 10–15% white opacity mimics
the specular highlight a light source would create at the upper edge of an elevated
card, and reinforces the depth cue without introducing the glowing-shadow problem.
A common pitfall is applying the same coloured tint for brand-coloured elevated surfaces. If your design uses a blue card or a green panel, the elevation tint needs to be neutral white, not a lighter shade of the panel's colour — otherwise the tinting system conveys brand emphasis rather than elevation, and the two signals conflict. Elevation is structural information; keep its signal clean.
Photographs do not automatically adapt to dark mode, and in dark-mode contexts
they often need a nudge. A bright, high-contrast image surrounded by a near-black
interface can look like a lamp in a dark room — visually jarring and attention-hoarding
in ways that break the intended content hierarchy. The standard correction is a subtle
brightness reduction: dropping image brightness to 85–90% in dark mode softens the
contrast with the surrounding interface without visibly degrading the image quality.
In CSS this is a single line — filter: brightness(0.9) scoped to the dark
theme — and it makes a disproportionate improvement to how the page reads as a whole.
Icons are a more delicate problem. Monochrome icons on a light background use dark fill; in dark mode you might invert to light fill, which generally works for purely geometric icons. The pitfall is icon sets with embedded optical adjustments — thin stroke icons whose visual weight was calibrated for a specific background tone. Inverting them directly often produces icons that read as too light and visually thin against a dark surface. The correct fix is to maintain separate dark-mode icon variants — slightly heavier stroke weight, adjusted to read at the same apparent weight as the light-mode versions despite the luminance difference. Most design systems skip this adjustment; most design systems have icons that look slightly off in dark mode without anyone being able to articulate why.
Video backgrounds, frequently used in hero sections, are the most aggressive problem. A bright video loop on a dark interface is almost always wrong — the contrast split creates an uncomfortable visual vibration that defeats both the video and the interface. Options in rough order of preference: source a version of the video graded for dark contexts; apply a dark overlay at 30–40% opacity on top of the video in dark mode; or simply disable the video background in dark mode and fall back to a static image with the same brightness treatment applied. The overlay approach is the fastest to implement but the least satisfying — it mutes the video for both modes equally rather than giving each mode a version that actually suits it.
Before you ship a dark mode implementation, run through every item on this list on a real device in a dark room. The simulator lies. The design tool lies. The room with the lights on lies. A physical screen in low ambient light will show you everything the other environments hide.