adam
Back to Blog
5 min read

OKLCH to the Rescue? Rethinking Color Systems for Modern UI Design

Why OKLCH is becoming essential for unified, maintainable, and accessible color systems in today's frontend stacks.

Main image for OKLCH to the Rescue? Rethinking Color Systems for Modern UI Design

Modern frontend development gives us heroic frameworks, lightning-fast build tools, and... color systems that are a mess. If your project’s colors are scattered across TypeScript files, hardcoded CSS, and JSON configs—join the club. From years wrangling dark mode and fighting pixel-perfect design drift, I’ve reached a stance: adopting OKLCH as your single source of truth for color isn’t just a trend—it’s the only sane path to maintainable, accessible, and future-proof UI systems. Below, I’ll show how the leap from hex to OKLCH fixed my project’s dark mode blues, unified my design tokens, and might just save your next redesign too.

Colors Run Amok: The Default State of Most Apps

Most apps begin innocently, with a couple of hex values in a config file. By the time dark mode rolls around, you’re patching colors.ts, adding hacks to your Tailwind config, and hardcoding semantic variables in CSS. That’s how my weight coaching app “Slope” grew a mild case of color schizophrenia: the same “brand blue” lived in three formats, and tweaking a background shade meant updating multiple files, with zero hints about what relationship those colors had (is #1e2330 darker than #1a1f2e? Go fetch your color meter).

The worst part: no matter how much fiddling I did, dark mode always looked a bit... Web 2.0. Hue-shifted off-brand, low-contrast, and nothing like the calm, unified interfaces we see in great design systems.

Enter OKLCH: Perceptual Predictability for the Rest of Us

Researching how modern design teams solve these pains, I stumbled on the emerging darling of color nerds: OKLCH (Oklab Lightness Chroma Hue). The pitch is persuasive: OKLCH is a perceptually uniform color space—colors with the same lightness value actually look the same brightness to our eyes (try asking hex for that kind of predictability).

For UI engineers, this is magical:

/* Both look equally bright, despite hue change */
--blue-500: oklch(0.55 0.20 250);
--red-500:  oklch(0.55 0.20 20);

You get L (Lightness: 0-1), C (Chroma: vibrancy), and H (Hue: angle in color space). Want deeper dark mode? Lower L. More pop? Raise C. Predictable, auditable, composable.

Color Design: Three Approaches, One Winner

Switching to OKLCH, I weighed three options for implementing tokens:

  • CSS-first: All colors as CSS custom properties, edited in one location and directly imported everywhere. No build process needed; one source of truth.
  • TypeScript tokens: Define in TS, generate CSS at build time for use elsewhere. More type-safety, but requires an extra build step and makes sharing awkward.
  • Hybrid: Keep hex for scales and OKLCH for semantic variables. "Best" of both worlds—or so it claims.

After playing with prototypes, my clear conclusion: CSS-first wins. With Tailwind v4 reading CSS variables natively, and modern CSS happy to import CSS, centralizing colors in a colors.css file is simpler, DRYer, and forwards-compatible. Most devs, if honest, don’t need color type safety—they need color sanity.

OKLCH in Action: Simple, Predictable, Modern

Here’s how this pattern now looks in my Slope app:

:root {
  /* Indigo scale */
  --primary-50: oklch(0.97 0.02 275);
  --primary-600: oklch(0.62 0.24 275);
  /* Semantic for light/dark */
  --semantic-light-background: oklch(0.96 0 0);
  --semantic-dark-background: oklch(0.15 0.03 275);
}

Globals CSS then maps these to app-wide variables:

@import "@hensonism/slope-tokens/colors.css";

@theme {
  --color-background: var(--semantic-light-background);
}

.dark {
  --color-background: var(--semantic-dark-background);
}

Tailwind utilities or semantic class names (bg-card, text-foreground) reference these variables everywhere. No double-entry; the right color propagates across the stack instantly.

Outcome: A Unified, Human-Friendly Color System

The upshot stunned me. Brand color tweaks—like shifting from a cyan-lean toward a refined indigo—were trivial, with all contrast and vibrancy calibrated by adjusting a single parameter. Cards in dark mode? No more trial-and-error: just lower the L value. The unpredictability, duplication, and accidental color drift of the previous system vanished.

Even IDE color preview, often cited as a reason against OKLCH, proved irrelevant once tokens were named and set: developers work with variables, not numbers. The cost is a tiny learning curve; the benefit is years of easier dark mode, better accessibility, and a stack your future self will actually understand.

Why OKLCH (and CSS-first) Is the Path Forward

If you're starting fresh, or can afford a modest refactor, OKLCH as a single source of truth is the obvious best practice—for maintainability, accessibility, and cost of future iteration. Old systems might remain hex by inertia, but every vector in modern CSS and design tools points toward OKLCH's eventual dominance.

  • Accessible by design: Predictable contrast means it’s easier to build for WCAG and pass audits.
  • Frictionless for dark mode: Shift lightness/hue, and your UI stays on-brand.
  • Future-friendly: Design handoffs reference the same tokens; one file to rule them all.

Don’t wait for the rest of the industry—embrace OKLCH and lose your color debt today.

Sources & Further Reading