kuler .ai
Home / Blog / The Dark Mode Palette Problem (And How to Solve It)
Dark Mode Color Theory

The Dark Mode Palette Problem (And How to Solve It)

Most dark mode implementations get color completely wrong. Here are the principles that actually work, with OKLCH examples.

· 6 min read

The naive approach (and why it fails)

The most common approach to dark mode is the simplest one: take your light mode palette, invert the lightness values, and ship it. White backgrounds become dark. Dark text becomes light. Done.

It never quite looks right, and here's why: color perception is not symmetric. The same hue at the same saturation reads differently on a dark background than it does on a light one. Your indigo brand color, vivid and readable at hsl(239, 84%, 67%) on white, becomes harsh and eye-straining on near-black. The math is mirror-symmetric, but the perceptual experience is not.

Beyond that, dark mode isn't just about swapping light for dark. It's about surface hierarchy — the visual language of depth and elevation. In light mode, shadows establish hierarchy. In dark mode, lighter surfaces rise above darker ones. These are different design systems, not inverses of each other.

Principle 1: Reduce chroma in dark mode

Saturated colors on dark backgrounds cause eye strain. The high contrast between a vivid hue and near-black creates visual vibration — the eye struggles to settle. The fix is to slightly reduce chroma (colorfulness) for your palette's dark mode variants.

In OKLCH, this is a precise operation:

/* Light mode — full chroma */
--color-primary: oklch(0.55 0.22 264);

/* Dark mode — same hue, same lightness, lower chroma */
--color-primary: oklch(0.65 0.16 264);

Notice the lightness actually goes up slightly in dark mode (0.55 → 0.65). This maintains sufficient contrast against the dark background while the reduced chroma (0.22 → 0.16) prevents harshness. The hue stays constant — the color still reads as clearly "your brand indigo."

Principle 2: Surface hierarchy through lightness steps

In a well-designed dark mode, surfaces get lighter as they get closer to the user (more interactive, more elevated). A four-step surface system works well:

  • Baseoklch(0.11 0.01 264) — The page background. Near-black, barely tinted with the brand hue.
  • Surfaceoklch(0.14 0.01 264) — Cards, panels. Slightly lighter.
  • Elevatedoklch(0.17 0.01 264) — Modals, dropdowns. One step above surface.
  • Overlayoklch(0.20 0.01 264) — Tooltips, popovers. The topmost layer.

The increments are small but intentional. Each step is clearly distinguishable, and the tiny brand-hue tint (C = 0.01) keeps everything from feeling sterile without adding visible color.

Principle 3: Semantic roles over absolute values

The biggest structural mistake in dark mode is defining colors by their value rather than their role. If you hardcode #6366f1 in your components, you need to override it everywhere in dark mode. If you use var(--color-primary), you only need to define it once in each theme.

A complete semantic color system has at minimum:

  • background — page base
  • surface — cards, panels
  • border — dividers, outlines
  • text — primary content
  • muted — secondary content, labels
  • primary — brand, main actions
  • primary-hover — interactive states
  • primary-subtle — backgrounds, highlights

Each of these gets a light and dark value. Your components reference roles, never raw hex. When the theme changes, everything changes with it.

Principle 4: Test contrast, not just appearance

Dark mode can feel fine in a bright office environment and become unreadable at night or on a phone with adjusted brightness. WCAG 2.1 AA requires a 4.5:1 contrast ratio for normal text — check every color pair, not just the obvious ones.

The most commonly missed pairs:

  • Muted text on surface backgrounds (often fails at 3–4:1)
  • Placeholder text in form inputs
  • Link colors against colored backgrounds
  • Icon-only controls with no text label

The kuler.ai Contrast Checker will tell you instantly whether any pair passes. Run it before you ship, not after.

The OKLCH advantage for dark mode

Everything described above is possible in HSL and RGB — but it requires more manual work. You're constantly eyeballing values and adjusting them by feel because the tools don't give you perceptual guarantees.

OKLCH gives you those guarantees. When you adjust L from 0.55 to 0.65, you know exactly how much lighter the color will appear. When you drop C from 0.22 to 0.16, you know exactly how much the saturation is decreasing. You're not guessing and checking — you're making intentional, predictable decisions.

For dark mode work specifically, where the relationships between surfaces and content colors are everything, that predictability is what separates a palette that works from one that almost works.

Back to blog