Skip to content
yutils

How Computers Represent Color (RGB, HSL, OKLCH)

What sRGB, P3, RGB, HSL, OKLCH actually mean. Why HSL feels intuitive but breaks visual consistency, why OKLCH is the new default in design systems, and how to read gamut and luminance like a pro.

~9 min read

What does #ff6b35 actually mean? Red 255 / green 107 / blue 53. So why does the same color look different on different monitors? Is hsl(20 100% 60%) the same color? When a designer says "let's switch to OKLCH," what changes? This guide walks through how computers describe color in numbers, the differences between RGB / HSL / OKLCH, what gamut means, and why OKLCH is becoming the new default in modern design systems.

Color is light (or ink dots)

A monitor pixel is three tiny LEDs — red, green, blue — that mix to produce every color. This matches the three cone types in the human retina (sensitive to roughly R/G/B). Each channel at 0-255 (8-bit) gives 16,777,216 colors.

Print works the opposite way — paper starts white, ink (CMYK) subtracts light. This guide stays on screen colors; CMYK is print-tool territory.

RGB / HEX — familiar, but not human-friendly

#ff6b35       ← HEX (6 hex digits)
rgb(255 107 53)  ← same color in CSS rgb

255 = R channel maxed
107 = G channel ~42%
 53 = B channel ~21%

Intuitive to the computer. To make "the same orange a little darker," a human has to adjust all three channels. Hue and lightness aren't separated, so visual tweaks are awkward.

Why hex?

Each channel 0-255 (1 byte) → two hex digits (00-FF). # + 6 digits = exactly 3 bytes. Compact for memory and URLs, and only uses URL-safe characters.

HSL — the first human-friendly attempt

hsl(20 100% 60%)
    │   │    │
    │   │    └─ Lightness 0-100%
    │   └────── Saturation 0-100%
    └────────── Hue 0-360 degrees

Hue is an angle on the color wheel: 0° = red, 120° = green, 240° = blue. Saturation is the distance from gray to fully saturated. Lightness moves between black and white.

The HSL gotcha — visual lightness ≠ HSL L

hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) — both L = 50%. But yellow looks far brighter. Human eyes weigh wavelengths differently (the luminosity curve), and HSL ignores this.

Consequence — in a design system, "any color at L 50% is equally bright" isn't true. Adjusting only L for a hover state produces different effect strength across colors.

OKLCH — perceptually uniform color (2020+)

Published by Björn Ottosson in 2020. Standardized in CSS Color 4. Now appearing in Tailwind v4, Radix, Tokens Studio.

oklch(70% 0.18 35)
       │    │   │
       │    │   └─ Hue (angle)
       │    └────── Chroma (saturation, 0~~0.4)
       └─────────── Lightness (perceptual, 0-100%)

Looks similar to HSL. The big difference: the L value reflects perceived brightness. oklch(70% 0.18 60) (yellow) and oklch(70% 0.18 240) (blue) actually look equally bright.

Why this matters:

  • Consistent hover/active states — dropping L by -10% across the token set darkens every color by the same amount
  • Intuitive contrast calculations — difference in L ≈ difference in perceived brightness
  • Automatic palette generation — same hue + varied L gives uniform shades/tints (used by Color Palette Generator)
  • Natural gradients — avoids the muddy gray midpoint of RGB interpolation (see the in oklch option in CSS Gradient Generator)

Gamut — not every color fits on every screen

Gamut is the range of colors a color space can represent. Common standards:

  • sRGB (1996) — the 30-year baseline. Covers roughly 35% of visible light. Supported everywhere.
  • Display P3 (2010+) — about 25% wider than sRGB. Modern Apple displays and HDR TVs support it. Reds and greens are noticeably more vivid.
  • Rec. 2020 — wider still. The 8K TV / HDR video standard. Not yet typical for general displays.

If OKLCH chroma exceeds sRGB gamut, the result depends on the screen — P3-capable screens show the wider color, others clamp to the nearest sRGB.

Using P3 colors in CSS

/* sRGB only */
.button { background: oklch(60% 0.2 25); }

/* More vivid on P3 displays */
.button {
  background: oklch(60% 0.2 25);
  background: oklch(60% 0.3 25) display-p3;
}

/* Or feature-detect with @media */
@media (color-gamut: p3) {
  .button { background: oklch(60% 0.3 25); }
}

Conversion — between color spaces

HEX ↔ RGB is just base conversion. RGB ↔ HSL is a deterministic formula. RGB ↔ OKLCH is more involved — gamma correction + passing through the Lab color space:

sRGB → linear RGB (undo gamma 2.2)
linear RGB → CIE XYZ
XYZ → OKLab (Lab transform)
OKLab → OKLCH (rectangular → polar)

Try it: Color Converter shows HEX / RGB / HSL / OKLCH side by side for the same color, so each space's character is easier to feel.

Accessibility contrast — relative luminance

WCAG contrast ratios (the 4.5:1 you've seen) use relative luminance: gamma-correct each RGB channel, then weighted sum:

L = 0.2126 * R + 0.7152 * G + 0.0722 * B
    (R/G/B as gamma-corrected 0-1 values)

The G weight (0.7152) dominates because human eyes are most sensitive to green. The same RGB total can look brighter if more of it is in G.

Color Contrast Checker computes the ratio and shows WCAG AA/AAA pass/fail immediately. OKLCH L and WCAG luminance are different metrics but both target "perceived brightness" — for most colors they agree.

Which space to use when

SituationRecommendedWhy
Legacy / simple color valueHEX / RGB100% support, familiar
Small hue adjustmentsHSLHue is intuitive enough for quick tweaks
Design system tokensOKLCHUniform L steps, automatic palettes
Vivid accents on P3OKLCH + P3 gamutSurface colors past sRGB
GradientsOKLCH (in oklch)No muddy midpoint
Accessibility verificationWCAG luminanceIt's the official metric

Common mistakes

1. The gray midpoint in RGB gradients

linear-gradient(to right, red, blue) interpolates in RGB and produces a dull midpoint. Add in oklch:

background: linear-gradient(in oklch to right, red, blue);

2. Assuming HSL L 50% = same brightness

Yellow vs blue, as above. If you build tokens in HSL, your hover states will feel uneven. OKLCH fixes that.

3. Picking the wrong color space in CSS color-mix

color-mix(in srgb, red 50%, blue)   /* dull middle */
color-mix(in oklch, red 50%, blue)  /* natural purple */

The first argument is the interpolation space. Default to OKLCH.

4. Dark mode via filter: invert

filter: invert(1) inverts RGB, turning brand colors into their complements. Maintain a separate token set for dark mode.

5. Saturation cranked to 100%

HSL S 100% strains the eye on most colors. UI palettes usually sit at S 50-80%, or in OKLCH around chroma 0.1-0.2.

References

Summary

  • RGB / HEX — computer-friendly, human-unfriendly. Maximum compatibility.
  • HSL — separates hue/sat/lightness for intuition. But HSL L does not equal perceived brightness (yellow vs blue).
  • OKLCH — perceptual L matches human eyes. New default for design systems, gradients, and palettes.
  • Gamut order: sRGB → Display P3 → Rec. 2020. P3 screens display more vivid colors.
  • Accessibility uses WCAG relative luminance — similar intent to OKLCH L but a different formula.
  • Try it: Color Converter / Color Palette Generator / Color Contrast Checker / CSS Gradient Generator.
Back to guides