AppCrib
Design Tools

OKLCH vs HSL vs LCH: Which Color Space Picks the Right Gradient Midpoint

Domain knowledge·Published by AppCrib··
GraduoCSS gradients with Tailwind output and OKLCH color quality.

Pick two colors on opposite sides of the wheel. Blue to yellow, say #3b82f6 to #facc15. Ask a CSS gradient to walk from one to the other. Then look at the middle.

In the default linear-gradient(...) in most browsers (which still interpolates in sRGB unless you tell it otherwise), the midpoint comes out muddy. A washed-out gray-brown band where you expected a clean transition. It's not a rendering bug. It's the color space doing exactly what it was designed to do, which turns out to be the wrong thing for gradients between distant hues.

The fix is to pick a different color space. CSS Color Module Level 4, which Chrome and Firefox both fully shipped in 2023, lets you specify the interpolation space directly: linear-gradient(in oklch, blue, yellow). The result is a gradient that passes through a recognizable mid-hue instead of dirt. Same start, same end, different math in the middle.

But "use OKLCH" is the version of the answer that fits in a tweet. Three color spaces all claim to be perceptually uniform, each with its own history, and there are a few cases where OKLCH isn't actually what you want.

What the muddy middle actually is

A gradient between two colors is a series of interpolated values: at each point along the gradient, you compute a blended color that's some weighted mix of the start and the end. The question is what "blended" means.

The naive approach is to blend the channels directly. If you're working in RGB, that means averaging the red channels, averaging the green channels, averaging the blue channels. Mathematically simple. Perceptually a mess. The reason is that RGB is what's called a *device-linear* space. It describes the physical signal sent to a display, not the perceptual experience of color. The midpoint of blue and yellow in RGB has roughly equal red, green, and blue components, which the human visual system reads as gray.

You'd think you could fix this by working in HSL, which separates color into hue, saturation, and lightness. HSL was designed in the late 1970s precisely because RGB is a bad authoring space. But HSL has its own problem: its hue dimension is a single circle around an unevenly spaced wheel. Interpolating hue between 240° (blue) and 60° (yellow) requires picking a direction around the wheel: short way through 0° (red-magenta territory), or long way through 180° (green-cyan territory). Browsers default to the shortest path, which often produces a transition through colors you didn't ask for. And HSL's lightness component is *nominally* linear but doesn't match perceived brightness at all. A 50% lightness yellow looks much brighter than a 50% lightness blue.

This is the "muddy middle." Not one problem, but a pile of small problems compounding. The interpolation space has the wrong axis structure for what you're trying to do, so any midpoint computed in that space ends up somewhere visually unhelpful.

What perceptual uniformity means, technically

The term "perceptually uniform color space" gets thrown around. It has a specific meaning, and the meaning matters for understanding why OKLCH and LCH exist.

A color space is perceptually uniform when equal numerical distances in the space correspond to equal *perceived* color differences. Move 10 units in any direction, see roughly the same amount of visual change. That property is what makes a color space useful for gradients: the midpoint of two colors in a perceptually uniform space lands at the perceptual midpoint, which is what your eyes expect.

The catch is that there's no single "correct" perceptually uniform space. Human color perception is complicated, varies between individuals, and depends on viewing conditions. Different mathematical models capture different aspects of it.

CIELAB, designed by the International Commission on Illumination (CIE) in 1976, was the first widely adopted attempt. Its cylindrical sibling LCH ("Lightness, Chroma, Hue") rearranges the same color data into a polar coordinate system, which is friendlier for hue-based work like gradients. LCH is reasonably uniform across most of its range but has known issues in the blue region: blues become unstable, hues shift unexpectedly during interpolation.

OKLAB, published by Björn Ottosson in 2020, addresses LCH's blue problem and a few other quirks. Its cylindrical form is OKLCH. The "OK" is for "okay," but the technical claim is that OKLCH is more perceptually uniform than LCH in the regions where modern interfaces actually live: saturated brand colors, mid-lightness body text, subtle UI tints.

Both LCH and OKLCH are usually better than HSL. OKLCH is usually better than LCH. None of them is right 100% of the time.

A direct comparison on a known-hard case

Here's the same gradient (#3b82f6, Tailwind blue-500, to #facc15, Tailwind yellow-400) interpolated in five different ways. The midpoint values are what you see at the 50% stop:

Color spaceMidpoint hexWhat you see
sRGB (default)#9da785A muddy olive-gray. The classic broken-gradient look.
HSL (short hue path)#9eff9cA bright mint green. Technically the midpoint, visually a surprise.
HSL (long hue path)#ff9effA magenta band. Browsers don't pick this by default; you'd have to force it.
LCH#cea972A warm tan. Better than sRGB but pulls toward orange.
OKLCH#bcb771A balanced khaki. Closer to what someone expects "halfway between blue and yellow" to mean.

None of these midpoints is identical to any of the others. None of them is "correct" in an absolute sense. There is no platonic midpoint of blue and yellow. The question is which one matches your design intent. For a gradient that's supposed to feel smooth, OKLCH usually wins. For a gradient where you want a visible hue transition through a specific intermediate color, HSL with an explicit longer hue keyword can be the right call. For pure technical correctness in cross-display rendering, LCH's deeper history of standardization still matters in some print and archival contexts.

When OKLCH is the wrong answer

This is the part that gets left out of most "just use OKLCH" guides.

Brand color matching. Designers who specify exact hex colors for brand consistency are working in sRGB whether they realize it or not. If your design system locks #FF6B35 as the primary orange, OKLCH conversion will produce a slightly different orange that lands a few perceptual units away from the spec. For brand-critical work, stay in sRGB or use OKLCH only for derived values (hover states, gradients between brand colors).

Older displays and color-managed workflows. OKLCH assumes a modern wide-gamut display behaves predictably. On older sRGB-only displays, OKLCH gradients are clipped back to sRGB at render time, and the clipping path isn't always perceptually uniform. The advantage shrinks. On color-managed print workflows, OKLCH is unfamiliar to print profiles, which generally speak in CIELAB. LCH is a better fit for those pipelines.

Animations with motion. Perceptually uniform color spaces feel "smoother" for static gradients, but the same property makes animated color transitions feel slightly slower. The human visual system is highly attuned to small color shifts; an OKLCH transition with equal perceptual steps will read as steadier than an HSL transition with the same duration, which can be the opposite of what a designer wants for a state change that's supposed to feel snappy. For UI motion, sRGB or HSL is often the right choice even though it's perceptually nonlinear.

Hue-locked design effects. Sometimes the artistic intent is to pass through a specific hue. A sunset gradient that goes blue → purple → red → orange → yellow needs HSL with a longer hue modifier, not OKLCH, because OKLCH's perceptually shortest path skips the purple band.

How CSS browsers actually implement this in 2026

CSS Color Module Level 4 specifies the interpolation space as part of the gradient syntax:

/* Default. Interpolates in sRGB unless told otherwise */
background: linear-gradient(to right, #3b82f6, #facc15);

/* Explicit OKLCH interpolation */
background: linear-gradient(in oklch, #3b82f6, #facc15);

/* OKLCH with a specific hue path */
background: linear-gradient(in oklch longer hue, #3b82f6, #facc15);

/* LCH for legacy compatibility */
background: linear-gradient(in lch, #3b82f6, #facc15);

The in oklch keyword is the part most authors miss. Without it, the browser falls back to sRGB interpolation even if the start and end colors are written in OKLCH notation. The color-space declaration applies to the *interpolation*, not the inputs.

Chrome shipped this as part of linear-gradient() and radial-gradient() in version 111 (March 2023). Firefox followed in version 113 (May 2023). Safari shipped the same syntax in 16.4 (March 2023). The CSS Working Group has a few outstanding questions on edge-case behavior (what happens at the OKLCH gamut boundary when one of the two colors is outside the display's gamut), but the basic interpolation behavior is stable across the three major engines.

What's *not* yet universal: Tailwind CSS 4 added native OKLCH support in its color system, but earlier versions (3.x and older) require you to specify the gradient with arbitrary value syntax (bg-[linear-gradient(in_oklch,...)]) and lose Tailwind's standard color shorthand. The ecosystem is in transition.

The math, briefly, for the curious

The OKLAB transform from linear sRGB is two matrix multiplications and a cube root. Given linear-light RGB values, you first apply a 3×3 matrix to get long-medium-short (LMS) cone response values:

L = 0.4122 * r + 0.5363 * g + 0.0515 * b
M = 0.2119 * r + 0.6806 * g + 0.1074 * b
S = 0.0883 * r + 0.2818 * g + 0.6300 * b

Then you take the cube root of each, which approximates the non-linear response of the human visual system to brightness:

l' = L^(1/3)
m' = M^(1/3)
s' = S^(1/3)

Then a second 3×3 matrix produces the final OKLAB coordinates. The cylindrical form OKLCH is just a polar coordinate transform of OKLAB's a and b channels: C = sqrt(a² + b²) for chroma, H = atan2(b, a) for hue.

That cube root is doing most of the perceptual-uniformity work. Linear sRGB values are proportional to light intensity at the display, but human perception of brightness is roughly the cube root of intensity (the Stevens power law for vision). Working in the cube-root space makes equal numerical differences correspond to equal perceived differences. LCH does the same thing with a slightly different exponent and a different matrix; OKLAB's contribution is mostly getting the matrix coefficients right for modern display gamuts.

The interpolation itself, once you're in OKLCH, is linear in lightness and chroma, and (depending on longer hue / shorter hue) takes one of two paths around the hue circle. Linear interpolation in a perceptually uniform space is what produces the visually smooth result.

What this means for picking colors

The practical takeaway, for someone shipping a CSS gradient this afternoon:

  • Default to OKLCH for any gradient between hues that are more than ~60° apart on the hue wheel. The muddy middle problem is worst there, and OKLCH handles it best.
  • For gradients within a single hue family (different shades of blue, different tints of red), the color-space choice barely matters. sRGB is fine.
  • For brand-matched solid colors, keep working in sRGB / hex. OKLCH for derived values only.
  • Always specify in oklch explicitly. The browser default is still sRGB.
  • Test on a real display. The math is one thing; the rendering pipeline through your monitor is another.

If you want to see the difference between color spaces side-by-side without writing CSS, Graduo does that. Pick two colors, watch the same gradient interpolated in sRGB, HSL, LCH, and OKLCH at once. Same start and end, four midpoints, on the same screen at the same time. Useful for any case where the "just use OKLCH" answer doesn't quite fit and you need to see what the alternatives look like before committing.

Graduo
CSS gradients with Tailwind output and OKLCH color quality.
Try Graduo