Articles

CSS Color Functions: oklch, lch, and color-mix() Explained

oklch(), lch(), and color-mix() let CSS describe color perceptually and blend it directly in the browser. How each works and when to reach for them.

Takina Takina · · 4 min read
Close-up of CSS syntax on a code editor screen

Modern CSS can describe color the way human vision actually perceives it, and blend two colors together without a preprocessor. oklch(), lch(), and color-mix() are the three functions that make this possible — they move color definitions off the old RGB grid and onto perceptually uniform models, and they let the browser do math on colors that used to require Sass or a design tool.

Why RGB and hex fall short

Hex codes and rgb() describe color as red, green, and blue channel intensities. That’s convenient for displays, which are literally built from red, green, and blue subpixels, but it’s a poor match for how people perceive color. Two colors with the same lightness value in RGB terms can look noticeably different in brightness to the human eye, and interpolating between two RGB colors — for a gradient or a hover transition — often passes through a muddy, desaturated band partway through.

hsl() improved on this by separating hue, saturation, and lightness, and it’s still common for quick tweaks. But HSL’s “lightness” channel still isn’t perceptually uniform: hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) share the same lightness value on paper yet look dramatically different in perceived brightness.

oklch() and lch(): perceptually uniform color

oklch() and lch() both describe color with three axes — lightness, chroma (roughly, saturation), and hue — but built on color models designed so that equal numeric steps produce equal perceived steps. oklch() is the newer, more refined version and is the one worth reaching for by default.

.button {
  background: oklch(65% 0.15 250);
}

That’s lightness (65%), chroma (0.15), and hue angle (250 degrees, landing in blue). The practical payoff:

  • Predictable lightness. Bump the first value and the color gets reliably lighter or darker, with no surprise saturation shift. This makes generating an entire shade scale from one base color straightforward — same hue and chroma, stepped lightness.
  • Smoother gradients and transitions. Interpolating between two oklch() colors avoids the gray, muddy midpoint that RGB and HSL gradients are prone to.
  • Wider gamut access. oklch() can address colors outside sRGB, which matters as more displays support wide-gamut color.

lch() works the same way but is built on the older CIELAB space; oklch() corrects some of CIELAB’s known perceptual distortions, so most new code should prefer it.

color-mix(): blending in the browser

color-mix() takes two colors and a color space and returns the blend — something that used to require Sass’s mix() function or a build step.

.hover-state {
  background: color-mix(in oklch, royalblue 70%, white);
}

This mixes 70% royalblue with 30% white, computed in the oklch color space. Common uses:

  • Deriving states from a base color. Instead of hand-picking a hover or disabled variant, mix the base custom property with white or black by a percentage.
  • Theming with a single source color. Store one brand color as a variable and derive tints and shades with color-mix() rather than maintaining a full manual palette.
  • Applying transparency without touching alpha syntax. Mixing a color with transparent produces the same visual result as lowering alpha, which is handy when the color value itself is a variable and you don’t want to rewrite its format.

The color space argument matters: mixing in srgb gives different (often duller) results than mixing in oklch or in oklab, because the interpolation path through the color space is different. For most UI work, oklch or oklab produce the more natural-looking blend.

Comparing the color functions

rgb() / hexhsl()oklch() / lch()color-mix()
DescribesChannel intensitiesHue, saturation, lightnessPerceptual lightness, chroma, hueA blend of two existing colors
Perceptually uniformNoPartiallyYesDepends on chosen space
Good for gradientsProne to muddy midpointsBetter, still imperfectSmoothSmooth when mixed in oklch/oklab
Wide-gamut colorsNoNoYesYes, if inputs are wide-gamut
Typical useLegacy code, exact brand hexQuick manual tweaksNew palettes, generated scalesDeriving states from a base color

Using them together

These functions compose well with CSS custom properties: store a brand color as a variable in oklch(), then use color-mix() to derive every hover, active, and disabled state from it at the point of use, instead of hardcoding a second color per state. That keeps a whole theme adjustable from one value, similar in spirit to how clamp() collapses a set of fixed breakpoints into one fluid expression — fewer hand-authored values, computed relationships instead.

Browser support for all three is solid in current evergreen browsers, but if a project still needs to support older engines, treat oklch()/color-mix() declarations as a progressive enhancement: declare an rgb() or hex fallback first, then the oklch() version after it. Non-supporting browsers use the first value they understand and ignore the rest — the same fallback-first pattern used when adopting other recent CSS features like container queries or :has().

The takeaway

oklch() describes color the way perception actually works, so lightness and hue changes behave predictably and gradients stay smooth. color-mix() moves palette derivation into CSS itself, blending two colors in a specified space without preprocessor math. Together they replace a good chunk of what used to require a Sass color module — store one base color, and let the browser compute the rest.

Takina Takina · · 4 min read

CSS object-fit and object-position, Explained

object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.

#CSS #Web Development #Frontend
Takina Takina · · 5 min read

CSS inherit, initial, unset & revert Explained

CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.

#CSS #Web Development #Frontend
Takina Takina · · 4 min read

Dynamic Viewport Units: dvh, svh, and lvh Explained

dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.

#CSS #Web Development #Frontend