UI/UX Atlas
Color Intermediate

Dark Mode Design (luminance-based elevation)

Craft dark themes that feel polished and accessible by using luminance steps to signal depth — not drop shadows that vanish on dark surfaces.

9 min read

Interactive example · Dark mode & elevation

Base surface

Level 0

Raised card

Level 1

Popover

Level 2

In light mode, elevation comes from progressively larger, softer drop shadows on white surfaces.

The full lesson

Most “dark modes” shipped over the past decade were afterthoughts. Designers flipped the background from white to black, inverted a few grays, and called it done. The result looked washed-out, lost its visual hierarchy, and introduced new accessibility failures.

A modern dark mode is a first-class theme. It has its own token values, its own elevation system, and its own contrast logic. Getting it right means understanding how human vision works in low-light conditions — and why the tricks that work in light mode silently break in the dark.

This lesson covers the perceptual and technical foundations of dark mode, the luminance-based elevation model that replaces shadows, the token architecture that makes theming maintainable, and the common pitfalls to catch before shipping.

Why Light-Mode Assumptions Break in the Dark

In a light-mode interface, the background radiates light and surfaces sit on top of it. Drop shadows work because a shadow is just a slightly darker region against a bright background. The higher the element, the larger the shadow’s blur and offset.

Flip to a dark theme and this logic breaks in a way that is easy to miss. A drop shadow on a dark surface sits against a near-black background. The darker-than-dark blur region becomes invisible. Drop shadows disappear in dark mode, leaving elevated components — dialogs, cards, dropdowns — looking flat and indistinguishable from the base layer.

The solution, popularized by Google’s Material Design 3 and backed by perceptual research, is luminance-based elevation. Surfaces at higher elevations are rendered with a slightly lighter fill than the base surface. This mirrors how real-world objects closer to a light source pick up more ambient light. Our visual cortex reads this lightness gradient as depth, no explicit shadow required.

Luminance-Step Elevation in Practice

The luminance-step model assigns a specific lightness value to each elevation tier. The scale below uses OKLCH notation. OKLCH distributes lightness perceptually — equal numeric steps look equally different to the human eye.

Elevation tierToken nameApprox. OKLCH lightnessTypical use
Base (0)surface-base10%Page background
Raised (1)surface-raised13%Cards, side panels
Overlay (2)surface-overlay17%Dropdowns, popovers
Modal (3)surface-modal21%Dialogs, sheets
Sunkensurface-sunken7%Input fields, code blocks

The steps are deliberately small — 3 to 5 percentage points each. Larger jumps read as stark color differences rather than depth. The sunken tier dips below the base to represent inset or recessed areas, like a text field well, reinforcing the inverse of elevation.

Tinted Surfaces

Some design systems — Material You, Atlassian’s dark theme — apply a subtle tint of the brand primary color to elevated surfaces instead of pure neutral gray. This is called a surface tint. The tint amount scales with elevation: a card might use a 5% primary-color overlay while a modal uses 12%. In OKLCH terms, the surface hue and chroma increase slightly with elevation while lightness stays the primary depth cue.

Tinted surfaces can unify the look and reinforce brand identity in dark mode. The risk: if the primary color is highly saturated, even small tint amounts look garish on dark backgrounds. Limit chroma to single digits — for example, oklch(17% 0.015 250) for the overlay tier. That value is imperceptible as a color difference but reads as a slight warmth or coolness.

Building the Token Architecture

A robust dark mode is not a second set of raw hex values. It is a second binding of semantic tokens to primitive values. The three-tier token hierarchy (W3C DTCG model) applies directly.

  1. Primitives — every discrete color in your palette. Neutral steps from 50 to 950, each brand hue with 12 or more lightness steps. Name these by their identity, not their role: neutral-100, violet-400.

  2. Semantic tokens — role-based aliases that reference primitives. These are the tokens components actually use: color.surface.base, color.surface.raised, color.text.primary, color.border.subtle. Each token has two values: one for light mode, one for dark.

  3. Component tokens — optional fine-grained overrides per component: dialog.surface, input.background.default.

In the W3C DTCG stable JSON format, a semantic token for surface-base looks like this:

{
  "color": {
    "surface": {
      "base": {
        "$type": "color",
        "$value": "{neutral.950}",
        "$description": "Page-level background in dark mode"
      }
    }
  }
}

The $value references a primitive token by its path. A separate theme file overrides the same token path for light mode, pointing to neutral.50 instead. Components reference only the semantic token, never the primitive directly.

Contrast in Dark Mode

WCAG 2.2 AA is the legal baseline for contrast: 4.5:1 for normal text, 3:1 for large text and UI components. The same ratios apply in dark mode, but the failure mode is different.

In light mode, text that is too light fails contrast. In dark mode, designers often use fully saturated brand colors for interactive text or icons and assume high chroma means high contrast. It does not.

A vivid oklch(65% 0.20 270) purple might achieve 4.8:1 against oklch(10% 0 0) — passing by a slim margin — while a similar hue at oklch(60% 0.20 270) drops to 3.9:1 and fails for body text. OKLCH makes this easier to reason about because lightness percentage tracks closely with perceptual luminance. Still, always verify with a contrast checker that uses the WCAG 2.2 relative luminance formula.

For quality beyond the binary pass/fail, APCA (Advanced Perceptual Contrast Algorithm) is a useful lens. APCA distinguishes between text on light versus dark backgrounds and accounts for polarity reversal. It is not a replacement for WCAG 2.2 — it has not been adopted as a normative standard — but running APCA alongside gives early warning when a combination technically passes WCAG while still feeling hard to read.

De-emphasis Without Failing Contrast

A common dark mode mistake is using very low-opacity text for secondary content — for example, white at 38% opacity on a dark background. Material Design’s older guidelines used exactly these values. The problem: 38% white over #121212 yields roughly #717171, which achieves about 4.5:1 and technically passes. But as surfaces lighten with elevation, the same 38% white over a modal at oklch(21% 0 0) can drop below 4.5:1.

The safer approach is to define absolute color values for each text role instead of opacity-based values. Use text.secondary pointing to neutral-400 in dark mode rather than rgba(255,255,255,0.38). You can verify these values once at design time instead of hoping every surface combination stays compliant.

Handling Color Perception Differences

Dark mode does not replace inclusive color design — it intersects with it. Two issues are specific to dark mode:

  • Saturated colors appear to bleed or glow on dark backgrounds (the Helmholtz–Kohlrausch effect). Red error states, green success indicators, and orange warning banners that look clean in light mode can appear to vibrate on dark surfaces. Reduce chroma by 15–25% for semantic status colors in dark mode tokens.

  • Deuteranopia and protanopia: the contrast between red and dark backgrounds is often worse than on white. An error state that relies solely on hue needs an icon or text label in all themes — but this is especially easy to miss in dark mode, where the visual salience of reds drops sharply.

Do

Define separate dark-mode token values for all semantic status colors (error, warning, success, info). Reduce chroma for dark mode variants. Back every status color with an icon or text label so information is never carried by color alone.

Don't

Copy light-mode status colors directly into the dark theme. Do not rely on raw opacity to create secondary text tones — test each combination as an absolute value against every surface tier it will appear on.

Respecting System Preference and User Choice

prefers-color-scheme: dark is the CSS media query that reflects the OS-level dark mode setting. Respecting it with no opt-out is a starting point, not the end state. Best practice is three-way preference support:

  1. System (default) — follows prefers-color-scheme
  2. Light — forced regardless of OS setting
  3. Dark — forced regardless of OS setting

Store the user’s explicit choice in localStorage (or a server-side session for authenticated users). Apply it before the first paint to avoid a flash of the wrong theme. The standard pattern is a small inline script in head — placed before any stylesheets — that reads localStorage and sets a data-theme attribute on html. CSS custom properties keyed to [data-theme="dark"] then take effect immediately.

CSS custom properties are the implementation layer for tokens. Declare the semantic token --color-surface-base with a light-mode value at :root, then override it inside [data-theme="dark"]. Components reference var(--color-surface-base) and the theme switch is instantaneous.

Testing and Auditing Dark Mode

Dark mode creates a second permutation of every visual component. A full dark mode audit covers:

  • Elevation hierarchy legibility — Can you visually tell apart all five elevation tiers (sunken, base, raised, overlay, modal) without hovering or interacting? If two tiers look identical, your lightness delta is too small.
  • Contrast sweep — Run every text/surface combination through a WCAG 2.2 contrast check. Automate this with Storybook’s accessibility addon or a CI step using axe-core.
  • Chromatic isolation — View the dark theme in a grayscale simulation. Every important distinction (success vs. error, primary button vs. secondary) must remain visible. If it disappears, the design relies on hue alone.
  • Shadow visibility — Intentional drop shadows, such as on tooltips where a border is not used, should be replaced with or supplemented by a subtle outer border (1px solid oklch(30% 0 0)) that stays visible on dark backgrounds.
  • Image and illustration handling — Photography on dark surfaces usually looks fine. Flat illustrations with white fills become invisible. Audit all embedded images and SVGs; provide dark-mode variants or convert white fills to transparent.
  • Focus rings — WCAG 2.2 SC 2.4.11 (Focus Not Obscured) and SC 2.4.12 (Focus Not Obscured Enhanced) require focus indicators to not be fully hidden. On dark backgrounds, a white focus ring that works in light mode may need a double-ring (white outer, dark inner) to stay visible on near-white surfaces inside dialogs.

Common Pitfalls Checklist

Before shipping a dark theme, run through this punch-list:

  • Pure #000000 background used instead of near-black → replace with oklch(10% 0 0) or equivalent
  • Drop shadows as the sole elevation signal → add luminance-step fills to surface tiers
  • Light-mode hex values directly negated (e.g., #F5F5F5 becomes #0A0A0A, #1A1A1A becomes #E5E5E5) → design the dark scale independently
  • Opacity-based secondary text not verified on every surface tier → convert to absolute token values
  • Saturated brand colors used at full chroma for interactive states → reduce chroma 15–25% for dark variants
  • Only prefers-color-scheme media query, no user-controlled toggle → add explicit light/dark/system preference UI
  • Theme applied with a class toggled after JS load → causes flash of wrong theme; move to inline head script
  • Focus rings identical to light mode → audit on all surface tiers, use double-ring pattern where needed