Take red — plain, full red. You can write it four ways: #FF0000, rgb(255, 0, 0), hsl(0, 100%, 50%), or oklch(0.628 0.258 29.2). It is still the same color, the same stream of light from the screen — only the language we use to describe it changes. But those four languages are not equal. Two of them are mathematically exact and mutually convertible down to the bit. One pretends to understand how you see — and lies. And one, the youngest, finally agrees with your eye. This is the story of how they differ.
How a screen makes color at all
A screen does not mix paint. It mixes light — additively, by summing. Every pixel is three independent subpixels: red (R), green (G) and blue (B). Fire all three at maximum and you get white; turn them all off and you get black. That is the opposite of painting, where each new layer of pigment subtracts part of the light, and the more paint you add, the closer you get to black.
In the most common architecture, each of the three channels is encoded in a single byte — and a byte holds 2⁸ = 256 levels of intensity, from 0 to 255. Three channels of 256 levels give 256³ = 16,777,216 combinations, the famous "16 million colors" from monitor brochures. On top of that sits a fourth channel, alpha (α), describing opacity — from 0 (fully transparent) to 1 (fully opaque).
It helps to separate two layers right away, because confusing them is the source of most misunderstandings about color. Splitting the signal into 256 levels and adding streams of light is an exact, physical fact. But choosing red, green and blue specifically as the primaries is a convention — an engineering compromise meant to stimulate the eye's three cone types (long-, medium- and short-wavelength) so as to imitate natural vision, even though a screen emits nothing like the sun's full spectrum. That boundary — what is exact versus merely a convenient convention — runs through the rest of this story.
HEX and RGB: the same color, a different alphabet
RGB and HEX are a direct reflection of what the screen does at the lowest level. rgb(255, 0, 0) means: red channel maxed out, the other two off. #FF0000 says exactly the same thing — only in hexadecimal. Each pair of HEX characters is one byte: FF in base 16 is 255 in decimal, 00 is zero. Hence the #RRGGBB pattern. Add an alpha channel and you get #RRGGBBAA: #FF000080 is red at roughly 50% opacity, because hexadecimal 80 is 128, about half of 255.
Converting HEX ↔ RGB is completely exact — a lossless swap of the same number between base 16 and base 10, without a shred of interpretation. So why do developers love HEX? For purely practical reasons: it is shorter (six characters instead of rgb(…)), supported since the dawn of HTML, and it exports cleanly from design tools. That is a decision about code ergonomics, not about the physics of light — the color itself is utterly indifferent to it.
HSL: color described the human way
Nobody thinks "I want 124 red, 58 green and 237 blue." We think "violet, heavily saturated, medium bright." That intuition is exactly what the HSL model, devised in the 1970s, tried to capture: its three coordinates are Hue, Saturation and Lightness.
- Hue is an angle on the color wheel, from 0° to 360°: 0° red, 120° green, 240° blue.
- Saturation is the purity of the color, from 0% (gray) to 100% (fullest color).
- Lightness runs from 0% (black) through 50% (the most vivid shade) to 100% (white).
HSL became wildly popular in interface design because it lets you manipulate color intuitively. Want a darker variant of a button on hover? In HSL you lower one parameter — L. In RGB or HEX you would have to recompute three independent channels at once. Mathematically HSL is still only a geometric transform of the RGB cube into a cylinder — exact and fully reversible. The trouble is that this geometry knows nothing about your eye.
The lightness lie
Here is the trap. Take yellow hsl(60, 100%, 50%) and blue hsl(240, 100%, 50%). According to HSL they have identical lightness: 50%. But look at them — the yellow is bright, almost luminous, the blue deep and dark. Your eye has no doubt that one is far lighter than the other, even though the number says they are equal.
This is not an illusion but a design flaw in HSL: its "lightness" is a purely geometric average of the channels, and the human eye does not respond equally to different wavelengths — we are far more sensitive to green-yellow than to blue. It becomes obvious the moment you write the same colors in perceptual oklch (more on it shortly): yellow lands at a lightness of 0.968, blue at just 0.452 — less than half. HSL claims "equal"; your eye and oklch say "nothing of the sort."
For aesthetics that is an inconvenience; for accessibility it is a serious mistake. Automatically generating contrasts for low-vision users (the WCAG guidelines) based on HSL's L is a methodological blunder, because changing only the hue at a fixed L can drastically change the real, perceived contrast of text against its background. Legibility math has to work on perceptual lightness, not geometric.
Light is not linear
There is another reason naive color math fails. The values in an sRGB notation (and thus in HEX and RGB) are not directly proportional to the physical amount of light. Between them sits so-called gamma correction — a nonlinear curve that compresses lightness so that the 256 available levels better match how the eye distinguishes dark from light tones (we are far more sensitive to subtle differences in shadows). Formally, an sRGB channel converts to linear light like this: for a value V ≤ 0.04045 it is V / 12.92, and above that, ((V + 0.055) / 1.055)^2.4.
The consequence is tangible. Average red rgb(255, 0, 0) and green rgb(0, 255, 0) "halfway" directly on the sRGB values, and the middle of the gradient comes out a muddy, dirty gray instead of a bright, luminous transition. Only converting to linear space (or, better still, to a perceptual one), averaging there, and converting back gives a natural result. Browser engines made exactly this mistake for years, blending colors in the wrong space.
oklch: color that thinks like the eye
In December 2020 the Swedish engineer Björn Ottosson published the OKLab color space — an answer to the flaws of both the old perceptual models (CIELAB from 1976 could turn blues "purple" when lightened) and the technical ones, sRGB/HSL. In everyday code we use its more convenient cylindrical form: oklch, written oklch(L C h / a):
- L — perceptual lightness, from 0 to 1. The key difference from HSL: the same value of L really is the same perceived brightness, regardless of hue.
- C — chroma, i.e. saturation measured as distance from gray. It starts at 0 and formally has no upper limit.
- h — the hue angle in degrees. Oriented differently than in HSL: in our converter red lands near ~29°, yellow ~110°, green ~142°, blue ~264°.
Historically oklch is the heir of the Munsell system of 1905, the first to order colors on the basis of experiments with human observers. The standard spread fast: it entered the CSS Color 4 specification, Adobe made it the default gradient-interpolation model in Photoshop, and Tailwind v4 built its palette generation on it. The reason is simple — once L matches real lightness, building consistent, accessible sets of colors stops being guesswork.
It is worth appreciating how neatly exactness and convention intertwine here. The conversions themselves — sRGB → linear light → OKLab → oklch — are hard algebra: matrix multiplication and cube roots, with no room for judgment. But the design of that space, the choice of weights, and the decision to "straighten out" lightness are pure perception: a numerical fit to experimental databases of human vision. It is engineering in the service of the eye.
Gamut: colors your screen cannot show
For over two decades the reference point was sRGB (a 1996 standard), covering only a fraction of the colors the eye can see. Modern OLED and mini-LED screens reach further — to the Display P3 space, which offers a noticeably larger color volume: deeper reds, brighter greens, and teals that sRGB simply cannot display.
And this is where oklch shows its edge. HEX codes and the rgb() function are structurally locked inside sRGB — you cannot write a color outside that range with them. But because chroma in oklch has no hard ceiling, the same oklch() function describes both a muted sRGB color and a super-saturated P3 one — you just raise C. When such a color has to be shown on an older screen, the browser maps the gamut in oklch precisely: it holds lightness (L) and hue (h) fixed and reduces only saturation (C) until the color fits into sRGB. As a result the color fades slightly but keeps its character — unlike the crude clipping of RGB channels, which can turn a vivid teal into an ordinary green. Our converter does exactly this: it shows an out-of-sRGB color exactly in oklch, and clamped in HEX/RGB/HSL, with a clear "outside sRGB gamut" flag.
The same color, four notations
The table below is not theory — these are the exact values from the color converter on this site. Look at the oklch column: for yellow and blue the HSL lightness is identical (50%), while the perceptual L diverges by more than two to one.
| Color | HEX | RGB | HSL | oklch |
|---|---|---|---|---|
| Red | #FF0000 | rgb(255, 0, 0) | hsl(0, 100%, 50%) | oklch(0.628 0.258 29.2) |
| Yellow | #FFFF00 | rgb(255, 255, 0) | hsl(60, 100%, 50%) | oklch(0.968 0.211 109.8) |
| Green | #00FF00 | rgb(0, 255, 0) | hsl(120, 100%, 50%) | oklch(0.866 0.295 142.5) |
| Blue | #0000FF | rgb(0, 0, 255) | hsl(240, 100%, 50%) | oklch(0.452 0.313 264.1) |
| Deep violet | #7F00FF | rgb(127, 0, 255) | hsl(270, 100%, 50%) | oklch(0.529 0.293 293.6) |
| Red at 50% α | #FF000080 | rgba(255, 0, 0, 0.5) | hsla(0, 100%, 50%, 0.5) | oklch(0.628 0.258 29.2 / 0.5) |
You can paste all six into the converter and see them live — including the checkerboard opacity preview for the last row.
Four languages, one color
The color on the screen is one. It is we who have four ways to talk about it — and each of them says something not about the color, but about us. HEX and RGB are closest to the machine: exact, mutually convertible, indifferent to how you see. HSL tries to speak the human way and pays for that convenience by missing the eye exactly where lightness matters. oklch pays off that debt — just as exact in its math, yet built around how we actually perceive light.
The whole art of working with color comes down to one distinction that has run through this entire text: what is mathematically exact (swapping notations, bytes, conversions) versus what is merely conventional and perceptual (the choice of primaries, the shape of the gamma curve, the design of oklch). You can change the notation all you like — the color does not change. But the choice of notation decides whether the math will be on your side, or against your eye.
Further reading
- Björn Ottosson, A perceptual color space for image processing (2020) — the source article on OKLab, with all the math: bottosson.github.io/posts/oklab.
- W3C, CSS Color Module Level 4 — the official specification for notation and wide-gamut color mapping: w3.org/TR/css-color-4.
- MDN Web Docs, oklch() — a practical guide to the CSS function: developer.mozilla.org.
- Evil Martians, OKLCH in CSS: why we moved from RGB and HSL — an implementation perspective with examples.
