Skip to main content
Back to all posts

Design

HEX, RGB, HSL, and OKLCH Explained: A Practical Color Format Guide

HEX and RGB are the same sRGB color in different notation, HSL is a human-friendly transform, and OKLCH is perceptually uniform. Here is when to use each.

MM H Tawfik10 min read

Every CSS color you write lands in one of a handful of notations, and most developers treat them as interchangeable trivia: pick whichever the design tool spat out. They are not interchangeable. #3B82F6, rgb(59 130 246), and hsl(217 91% 60%) describe the exact same pixel, but each format makes a different operation easy — and one of them, OKLCH, fixes a perceptual bug the other three have shipped with for thirty years.

This guide draws the lines precisely. We cover what each format actually stores, when to reach for which, and why the newest one — OKLCH from CSS Color Module 4 — is the one worth learning even though it looks unfamiliar.

TL;DR: how do HEX, RGB, HSL, and OKLCH differ?

HEX and RGB are the same sRGB color in different notation — HEX writes each channel as a hexadecimal byte (00FF), RGB writes it as a decimal 0255. HSL is a human-friendly transform of that same RGB — it re-expresses the color as a hue angle, a saturation percentage, and a lightness percentage, so tints and shades are trivial to derive. OKLCH is a perceptually-uniform space (CSS Color 4) where equal numeric steps look equally different to the human eye, and which can reach wide-gamut (P3) colors the others cannot express.

The one-sentence decision rule:

  • Use HEX/RGB for fixed brand values and copy-paste from design tools.
  • Use HSL when you want to nudge lightness or saturation by hand.
  • Use OKLCH for color scales, gradients, and theming where perceptual evenness matters.

Everything below is detail. You can convert any value between all four with the color converter.

What is HEX color?

A HEX color is three bytes written in hexadecimal: #RRGGBB, where each pair is one channel from 00 (0) to FF (255). It is not a separate color model — it is sRGB, just spelled in base 16 because two hex digits map cleanly to one byte.

Take #3B82F6. Split it into pairs and convert each from hex to decimal:

3B → 3×16 + 11 = 59     (red)
82 → 8×16 + 2  = 130    (green)
F6 → 15×16 + 6 = 246    (blue)

#3B82F6 → rgb(59, 130, 246)

That is the entire HEX→RGB algorithm. There is no rounding and no information loss in either direction — HEX and RGB are byte-for-byte the same color.

Two shorthands you will meet:

  • #RGB (3-digit) expands by duplicating each digit. #f00 means #ff0000rgb(255, 0, 0), pure red. #0f8 is #00ff88. This only works when both digits of every channel are equal.
  • #RRGGBBAA (8-digit) adds an alpha byte for opacity. #3B82F680 is #3B82F6 at alpha 80 hex = 128/255 ≈ 50% opacity. The 4-digit #RGBA shorthand follows the same expansion rule.

HEX is the lingua franca of design handoff — Figma, Photoshop, and brand guidelines all default to it — but it is the worst format for editing a color by hand, because the digits have no intuitive relationship to brightness or hue.

What is RGB color?

RGB is the additive light model: every color is a mix of red, green, and blue light, each channel ranging 0255. Zero of all three is black; full of all three is white. It is additive because you are adding emitted light — the opposite of mixing paint, where adding pigments moves toward black.

rgb(59, 130, 246) is the same blue as #3B82F6, just in decimal. The legacy syntax uses commas, with rgba() for opacity:

color: rgb(59, 130, 246);
color: rgba(59, 130, 246, 0.5);   /* legacy, comma + alpha */

CSS Color 4 introduced a cleaner space-separated syntax that unifies rgb() and rgba() and uses a slash for alpha:

color: rgb(59 130 246);            /* modern: no commas */
color: rgb(59 130 246 / 50%);      /* alpha after a slash */
color: rgb(59 130 246 / 0.5);      /* fraction also valid */

The modern form is preferred — rgba() still works for backwards compatibility, but there is no longer a reason to use a separate function name for the alpha case. RGB is great for programmatic manipulation (you can do math on raw channels) and exact device values, but like HEX, the numbers tell you nothing perceptual: is rgb(180, 180, 0) brighter than rgb(0, 0, 255)? You cannot tell by reading it.

What is HSL color?

HSL re-expresses the same sRGB color as three human-meaningful axes: Hue (an angle 0360° around the color wheel), Saturation (a percentage from gray to vivid), and Lightness (a percentage from black to white). It is a direct mathematical transform of RGB — no new colors, just a friendlier coordinate system.

The hue wheel: /360° is red, 120° is green, 240° is blue. Saturation 0% is always gray regardless of hue; 100% is the most vivid that hue gets. Lightness 0% is black, 50% is the "pure" color, 100% is white.

Converting our blue rgb(59, 130, 246) to HSL gives:

hsl(217 91% 60%)        /* same blue as #3B82F6 */
hsl(217 91% 60% / 50%)  /* with alpha, modern slash syntax */

The whole point of HSL is editing without a color picker. Want a lighter tint of that blue for a hover state? Bump the lightness:

--blue:        hsl(217 91% 60%);
--blue-tint:   hsl(217 91% 75%);   /* lighter — just raise L */
--blue-shade:  hsl(217 91% 45%);   /* darker  — just lower L */
--blue-muted:  hsl(217 40% 60%);   /* desaturated — lower S */

That is far more intuitive than guessing new HEX digits. HSL is the workhorse for hand-tuned design systems. Its weakness is the reason OKLCH exists, which is next.

What is OKLCH, and why does it matter?

OKLCH is a perceptually-uniform color space defined in CSS Color Module 4, where equal numeric changes produce equal perceived changes. Its three axes are L (perceptual lightness, 01 or 0%100%), C (chroma, roughly how vivid, unbounded but practically 00.4), and H (hue angle 0360°). The oklch() function is built on the OKLab model published by Björn Ottosson in 2020.

The problem it fixes is real and you have already hit it. In sRGB and HSL, lightness is a lie. hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) both claim 50% lightness, but yellow is blindingly brighter than that deep blue. So when you build a color scale or interpolate a gradient in HSL, the steps look uneven — bright in the yellows, muddy and dark in the blues and purples. Mid-tones of a gradient between two saturated colors go gray and dingy because the path cuts through the desaturated center of the model.

OKLCH was engineered so that equal L values look equally bright across every hue, and so that interpolating between two colors stays vivid. The same blue in OKLCH:

color: oklch(0.62 0.19 256);          /* ≈ #3B82F6 */
color: oklch(0.62 0.19 256 / 50%);    /* with alpha */

Two concrete wins:

1. Even color scales for free. Hold chroma and hue, step lightness evenly, and you get a palette where every stop is one perceptual notch apart:

--blue-100: oklch(0.95 0.03 256);
--blue-300: oklch(0.80 0.10 256);
--blue-500: oklch(0.62 0.19 256);   /* the base */
--blue-700: oklch(0.48 0.16 256);
--blue-900: oklch(0.34 0.10 256);

Tailwind CSS v4 moved its entire default palette to OKLCH for exactly this reason.

2. Wide-gamut (P3) color. sRGB — and therefore HEX, RGB, and HSL — can only describe colors inside the sRGB triangle. Modern displays cover the larger Display-P3 gamut with noticeably more saturated greens and reds. OKLCH can address those colors directly; a high-chroma value like oklch(0.7 0.32 145) reaches a green sRGB simply cannot encode. Gradients also interpolate in OKLCH by default in CSS Color 4, killing the muddy-midpoint problem.

Browser support is now broad. Per caniuse, oklch() is supported in all current versions of Chrome, Edge, Safari, and Firefox (shipped across the board through 2023). For older browsers, provide a HEX/RGB fallback declaration above the oklch() one — CSS ignores rules it does not understand, so the last valid one wins.

HEX vs RGB vs HSL vs OKLCH: the comparison table

| Format | Syntax example | Best for | Gamut | |--------|----------------|----------|-------| | HEX | #3B82F6 | Brand values, design-tool handoff, copy-paste | sRGB | | RGB | rgb(59 130 246 / 50%) | Programmatic channel math, exact device values | sRGB | | HSL | hsl(217 91% 60%) | Hand-tuning tints, shades, and saturation | sRGB | | OKLCH | oklch(0.62 0.19 256) | Perceptual color scales, gradients, P3 wide gamut | Wide (P3+) |

All four rows describe the same blue (within the sRGB rows exactly; OKLCH to ~2 decimal places). Paste any of them into the color converter to see the others instantly, or sample colors off your screen with the color picker.

What about contrast and accessibility?

Color choice is not just aesthetic — text must stay readable. The WCAG 2.1 contrast minimums require a ratio of at least 4.5:1 for normal body text (AA) and 7:1 for AAA; large text relaxes to 3:1. Crucially, WCAG's contrast formula is defined on sRGB luminance, so it works the same whatever notation you authored the color in — HEX, RGB, HSL, and OKLCH all collapse to the same underlying color before the ratio is computed.

A frequent trap: HSL lightness is not the same as WCAG luminance. Two colors at hsl(... 50%) can have wildly different contrast against white, because lightness ignores how the eye weights green over blue. This is another place OKLCH helps — its L tracks perceived lightness far more closely — but you should still verify the actual ratio. Check any foreground/background pair against the AA and AAA thresholds with the color contrast checker before shipping.

Which format should I actually use?

Pick by the operation you are doing, not by habit:

  1. Storing a brand color or pasting from Figma? HEX. It is universal and lossless for sRGB.
  2. Doing arithmetic on raw channels in JS or animating a single channel? RGB, modern space-separated syntax.
  3. Hand-deriving a hover/active/disabled variant of one color? HSL — change one number.
  4. Building a full palette, an even gradient, or supporting wide-gamut displays? OKLCH, with a HEX fallback for ancient browsers.

In a real design system the answer is usually "OKLCH for the generated scale, HEX in the documentation." Generate that scale with the color palette generator, blend two colors with the color mixer, and build smooth multi-stop backgrounds with the CSS gradient generator.

TL;DR

HEX and RGB are the identical sRGB color in two notations — hex bytes versus 0255 decimals — with a clean, lossless conversion (#3B82F6 = rgb(59 130 246)). HSL re-expresses that same color as hue/saturation/lightness so you can hand-derive tints and shades by changing one number. OKLCH, from CSS Color 4, is the perceptually-uniform upgrade: equal numeric steps look equally different to the eye, gradients stay vivid instead of going muddy at the midpoint, and it reaches wide-gamut P3 colors the sRGB trio cannot encode — now supported in every major browser. Author brand values in HEX, hand-tune in HSL, and build palettes and gradients in OKLCH. Always verify text against the WCAG 4.5:1 (AA) minimum, because lightness is not luminance.

Convert between all four formats with the color converter, sample with the color picker, and check readability with the color contrast checker — or browse every utility in the tools directory.