Skip to article
Görkem Yıldız, home
Available for October

OKLCH vs Helmlab: Which Color Space Models Human Vision Better?

Comparing OKLCH and Helmlab color spaces on STRESS accuracy, parameter count, and perceptual modeling. Which one matches human vision better?

· 10 min readEnglishTürkçe oku

OKLCH has been one of the most important additions to CSS in recent years. With CSS Color Level 4, frontend developers finally gained access to a perceptual color space right in their stylesheets. But how well does OKLCH actually model human vision? And what happens when you need more than 18 parameters can offer?

This post is a deep comparison between Oklab/OKLCH and Helmlab -- two perceptual color spaces built on very different philosophies. We look at measurable performance, practical code examples, and when you should reach for each one.

What Is OKLCH and Why Does It Matter?

Oklab, created by Bjorn Ottosson in 2020, is a perceptual color space designed for simplicity. OKLCH is its cylindrical representation (Lightness-Chroma-Hue), and it became part of the CSS Color Level 4 specification. Every modern browser supports it natively:

Code
/* OKLCH in CSS */
.button-primary {
  background: oklch(0.65 0.18 250);       /* L=0.65, C=0.18, H=250 (blue) */
}

.button-primary:hover {
  background: oklch(0.55 0.18 250);       /* Only lightness changed */
}

.surface {
  background: oklch(0.97 0.01 250);       /* Very light blue surface */
}

What Oklab got right:

  • Perceptual uniformity -- Dramatically better than HSL. Equal L values across different hues produce similar perceived brightness.
  • Simplicity -- 18 parameters total: a 3x3 matrix and cube roots. Easy to understand and implement.
  • CSS integration -- oklch() works in all modern browsers with zero JavaScript.
  • Hue stability -- Changing lightness causes far less hue shift compared to HSL.

Oklab was genuinely a leap forward. But color science covers far more territory than 18 parameters can map.

Helmlab: 72 Parameters, 13 Stages

Helmlab is a perceptual color space trained on over 64,000 human color-difference observations. Instead of Oklab's "good enough" approach, it includes dedicated pipeline stages for every known quirk of the human visual system:

  • Helmholtz-Kohlrausch effect -- Saturated colors appear brighter than neutral grays at the same luminance. A vivid red looks "lighter" than a medium gray, even when their physical luminance is identical. Helmlab models this directly as a chroma-dependent lightness shift embedded in the L axis. Oklab does not account for this.
  • Hue correction -- A 4-harmonic Fourier series calibrates and equalizes the perceptual hue circle. Each hue region is independently adjusted.
  • Neutral correction (NC) -- Mathematically guarantees that gray tones map to a=b=0. Critical for palette generation and gamut mapping.
  • Rigid rotation -- A -28.2 degree rotation in the ab-plane aligns hue angles with intuitive positions, without changing the STRESS metric.
  • Dark L compression -- Hue-dependent correction in dark colors. Dark blues and dark yellows need fundamentally different treatment.
  • Minkowski distance metric -- Instead of simple Euclidean distance, Helmlab uses CIEDE2000-style pair-dependent weights learned from data.

The full pipeline:

XYZ → M1 → power → M2 → hue_correction → H-K → cubic_L → dark_L → chroma_scale → chroma_power → L_chroma → NC → rotation

Every stage is exactly invertible. The inverse transform from Lab back to XYZ uses Newton iteration where needed.

STRESS Comparison: What the Numbers Say

STRESS (Standardized Residual Sum of Squares) measures how well mathematical distances in a color space predict actual human perceptual judgments. Lower is better. This metric is evaluated against 64,000+ human observations from six major color-difference datasets.

STRESS Comparison (lower = better) Color Space STRESS Parameters CIE Lab ~30.3 12 CIEDE2000 29.2 17 (+Lab) Oklab / OKLCH ~27.5 18 Helmlab v19 23.2 72 Helmlab: 20% better than CIEDE2000, 16% better than Oklab at predicting human perception

This is not an abstract number. Bootstrap confidence intervals show Helmlab's 95% CI at [22.50, 23.93] versus CIEDE2000's [27.64, 30.84]. The distributions do not overlap -- this is a statistically significant difference.

Practical Difference: Palette Generation

Numbers are important, but the real question is: what does this mean in practice? The most visible impact is in palette generation.

Palettes with OKLCH

In OKLCH, you create a blue palette by varying lightness:

Code
/* Blue palette with OKLCH */
:root {
  --blue-100: oklch(0.95 0.05 250);
  --blue-300: oklch(0.75 0.12 250);
  --blue-500: oklch(0.60 0.18 250);
  --blue-700: oklch(0.40 0.18 250);
  --blue-900: oklch(0.25 0.12 250);
}

This is dramatically better than HSL. But two problems remain:

  • Helmholtz-Kohlrausch effect -- At high chroma values, equal lightness values produce different perceived brightness for different colors. OKLCH cannot compensate for this.
  • Step uniformity -- The perceptual difference between L=0.40 and L=0.60 may not match the difference between L=0.75 and L=0.95.

Palettes with Helmlab

The Helmlab npm package generates perceptually equidistant palettes automatically:

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// One-line Tailwind-style semantic scale
const scale = hl.semanticScale('#3B82F6');
// {
//   '50':  '#b3c7ff',   // lightest
//   '100': '#aabdff',
//   '200': '#96aaff',
//   '300': '#809aff',
//   '400': '#628eff',
//   '500': '#3b82f6',   // base color
//   '600': '#0263ca',
//   '700': '#0048a2',
//   '800': '#003179',
//   '900': '#00184b',
//   '950': '#000b2f',   // darkest
// }

// Steps are perceptually equidistant:
const d1 = hl.deltaE(scale['300'], scale['400']);
const d2 = hl.deltaE(scale['700'], scale['800']);
// d1 and d2 are close (within +/-5%)

The difference is that Helmlab accounts for the Helmholtz-Kohlrausch effect, dark region compression, and hue-dependent chroma scaling at every step. The result: the gaps your eyes see are actually equal.

DeltaE Comparison: Distance Accuracy

The most direct way to evaluate color spaces is to compare the "distance" they predict between two colors against actual human experiments. Below, we calculate distances for several color pairs in both spaces:

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Color pairs and perceptual distance comparison
const pairs = [
  { name: 'Red vs Green',      a: '#e74c3c', b: '#2ecc71' },
  { name: 'Blue vs Teal',      a: '#3498db', b: '#1abc9c' },
  { name: 'Yellow vs Orange',  a: '#f1c40f', b: '#e67e22' },
  { name: 'Black vs Dark Gray', a: '#000000', b: '#333333' },
  { name: 'White vs Light Gray', a: '#ffffff', b: '#cccccc' },
];

for (const { name, a, b } of pairs) {
  const dist = hl.deltaE(a, b);
  console.log(`${name}: deltaE = ${dist.toFixed(3)}`);
}

// Helmlab distances match human experimental ratios
// 16% more accurately than Oklab
Sample Color Pairs -- Perceptual Distance Color Pair Colors Oklab dE Helmlab dE Red vs Green 0.476 0.891 Blue vs Teal 0.153 0.239 Yellow vs Orange 0.128 0.174 Black vs Dark Gray 0.133 0.125 White vs Light Gray 0.069 0.063

What matters is not the absolute values, but the rankings and ratios. Helmlab distance estimates are better proportioned to the ratios found in human experiments. For example, both spaces know that red-green is much more different than yellow-orange, but Helmlab's ratio matches the human experimental ratio more closely.

The Helmholtz-Kohlrausch Effect: OKLCH's Blind Spot

This effect is one of color science's best-known but least-modeled phenomena. The basic principle is simple: highly saturated colors appear brighter than neutral grays at the same luminance.

The practical consequences are significant:

  • A vivid red and a medium gray at the same OKLCH lightness value will look different in brightness when placed side by side on screen.
  • In palette generation, high-chroma colors "drift" in perceived lightness -- your 500-level color looks brighter than expected.
  • In contrast calculations, the perceived brightness of saturated colors is not adequately accounted for.

Helmlab models this effect directly in its pipeline, applying a chroma-dependent L shift during every color conversion. The result: a saturated red at L=0.5 and a gray at L=0.5 actually have the same perceived brightness in Helmlab coordinates.

Code Examples: Working with Helmlab

Installation

Terminal
npm install helmlab

sRGB Conversions

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Direct conversion from sRGB [0,1]
const lab = hl.fromSrgb([0.231, 0.510, 0.965]);  // Tailwind blue-500
console.log(lab);
// [0.625, -0.084, -0.310]  (L, a, b tuple)

// Hex conversions
const labFromHex = hl.fromHex('#3B82F6');
const backToHex  = hl.toHex(labFromHex);        // '#3b82f6'

DeltaE: Measuring Color Difference

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Perceptual distance between two colors
const distance = hl.deltaE('#3B82F6', '#2563EB');
console.log(`Delta E: ${distance.toFixed(4)}`);

// Similar shades should be close, different hues should be far
const close = hl.deltaE('#3B82F6', '#60A5FA');   // ~0.15
const far   = hl.deltaE('#3B82F6', '#EF4444');   // ~0.85

// Distance rankings match human perception:
// close < far (blue shades are near each other, blue-red is far)

Semantic Scale Generation

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Tailwind CSS-style 50-950 scale
const blueScale = hl.semanticScale('#3B82F6');

// Custom step counts
const fiveStep = hl.semanticScale('#3B82F6', [100, 300, 500, 700, 900]);
// {
//   '100': '#aabdff',
//   '300': '#809aff',
//   '500': '#3b82f6',
//   '700': '#0048a2',
//   '900': '#00184b'
// }

// Build a full design system
const tokens = {
  blue:   hl.semanticScale('#3B82F6'),
  red:    hl.semanticScale('#EF4444'),
  green:  hl.semanticScale('#22C55E'),
  amber:  hl.semanticScale('#F59E0B'),
};

// Every step of every color is perceptually consistent

WCAG Contrast Checking

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Calculate contrast ratio
const ratio = hl.contrastRatio('#3B82F6', '#ffffff');
console.log(`Contrast: ${ratio.toFixed(2)}`);     // 3.68

// Auto-adjust to meet AA standard
const accessible = hl.ensureContrast('#3B82F6', '#ffffff', 4.5);
// Helmlab adjusts along the lightness axis, preserving hue and chroma

// WCAG level checks
hl.meetsContrast('#000000', '#ffffff', 'AA');      // true
hl.meetsContrast('#3B82F6', '#ffffff', 'AA');      // false (3.68 < 4.5)
hl.meetsContrast('#1D4ED8', '#ffffff', 'AA');      // true

OKLCH or Helmlab? When to Use Which

These two spaces are not competitors -- they operate at different layers of your stack.

OKLCH is ideal when:

  • Pure CSS color definitions -- Defining perceptual colors directly in CSS without any JavaScript.
  • Simple color adjustments -- Nudging lightness or hue by small amounts.
  • Browser compatibility -- When no additional library or build step is acceptable.
  • Learning curve -- A great entry point for developers new to color science.

Helmlab excels when:

  • Design system palettes -- When every step of a 50-950 scale must be truly equidistant.
  • Contrast calculations -- When the H-K effect must be accounted for in accessibility checks.
  • Color difference comparisons -- When you need an accurate answer to "how similar are these two colors?"
  • Production-grade color processing -- Applications requiring the highest color accuracy available.
  • Gamut mapping -- When colors need to be correctly mapped within sRGB or Display P3 boundaries.

Using Them Together

The ideal approach is to use both. Define base colors with oklch() in CSS, then use Helmlab for palette generation, distance calculations, and contrast guarantees:

Code
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// 1. Define and analyze your brand color with Helmlab
const brandBlue = '#3B82F6';
const info = hl.info(brandBlue);
console.log(`L: ${info.L.toFixed(3)}, C: ${info.C.toFixed(3)}, H: ${info.H.toFixed(1)}`);

// 2. Generate all palette steps with Helmlab
const palette = hl.semanticScale(brandBlue);

// 3. Verify contrast at every step
for (const [level, hex] of Object.entries(palette)) {
  const onWhite = hl.contrastRatio(hex, '#ffffff');
  const onBlack = hl.contrastRatio(hex, '#000000');
  console.log(`${level}: ${hex} | white: ${onWhite.toFixed(1)} | black: ${onBlack.toFixed(1)}`);
}

// 4. Export as CSS custom properties
const cssVars = Object.entries(palette)
  .map(([k, v]) => `  --blue-${k}: ${v};`)
  .join('\n');
console.log(`:root {\n${cssVars}\n}`);

Technical Comparison Table

Oklab/OKLCH vs Helmlab -- Technical Comparison Feature Oklab/OKLCH Helmlab Parameters 18 72 Pipeline stages 3 (matrix + root) 13 STRESS ~27.5 23.2 H-K effect model None Embedded Neutral axis guarantee Structural NC (error < 10⁻⁶) Distance metric Euclidean Minkowski + SL/SC CSS integration oklch() native npm package Bundle size 0 KB (native) 10.3 KB gzip

Conclusion

OKLCH was a milestone for CSS -- it brought perceptual color to the browser, and it remains the best built-in color space available in stylesheets. But an 18-parameter model cannot fully capture the complexity of the human visual system.

Helmlab, trained on 64,000 human observations with 72 parameters, predicts color differences 16% more accurately than OKLCH and 20% more accurately than the industry-standard CIEDE2000. Features like the Helmholtz-Kohlrausch effect, neutral correction, and hue-dependent calibration make a concrete difference in palette generation and contrast calculations.

With a 10.3 KB npm package, it integrates into any JavaScript project in a few lines:

Terminal
npm install helmlab

Source code: GitHub | Package: npm

This is post 1 of 16 in the Helmlab series. Next up: HSL vs Helmlab -- why HSL falls short for design system palettes.

Görkem Yıldız

Helmlab, a family of perceptual color spaces for UI design systems (arXiv:2602.23010)

I'm Görkem, a UI/UX designer and engineer based in Ankara, leading the design team at [PushBrands](https://pushbrands.app/en), a Saudi UGC marketplace. I've spent over eight years designing and building digital products, and lately the systems underneath them: Helmlab, a color space trained on 64,000 human color judgments that now ships in Color.js; open-source tools for icons, sound and the terminal; and, as Orbit Design, Figma plugins with more than 100,000 users.

Book a call

Talk about color, design systems or Helmlab.

Saturdays 19:00–21:00 Istanbul time, on Google Meet.

Book a call