Every color you put on a web page ends up as the same three numbers — red, green, and blue — but the syntax you write it in changes how easy your CSS is to read, tweak, and theme. HEX, RGB, and HSL all describe the same colors; they just hand you different dials to turn.
This is a working developer's tour of all three: how the math actually works, where alpha fits in, why HSL is often the format you want to think in, and how to move between them without a lookup table.
The shared foundation: 24-bit RGB
Screens emit color by mixing red, green, and blue light. On the web, each channel gets 8 bits — an integer from 0 to 255, or 256 possible levels. Three channels give you 256 × 256 × 256 ≈ 16.7 million colors. That's the entire sRGB palette the browser works with, and HEX,
rgb()hsl()Keep that in mind: none of these formats is "more powerful" than the others within sRGB. They round-trip to the same pixels. The differences are ergonomic.
HEX: compact and ubiquitous
A HEX color is the three RGB channels written in base 16. Each channel takes two hex digits (
00ffcss.button { color: #1a73e8; /* R=1a(26) G=73(115) B=e8(232) */ background: #ffffff; /* white */ border-color: #000; /* shorthand for #000000 (black) */ }
The three-digit shorthand (
#000#f0c#f0c#ff00cc#f00cHEX is everywhere because it's terse and copy-pasteable. Design tools export it, it's the default in most pickers, and it survives being passed around in chat and tickets. The downside is that it's opaque to humans:
#7c3aedAlpha in HEX
Modern CSS supports an optional fourth channel — alpha (opacity) — as a third or fourth pair of hex digits:
css.overlay { background: #00000080; /* black at 50% (0x80 = 128 ≈ 0.5) */ } .tint { background: #f0c8; /* 4-digit shorthand → #ff00cc88 */ }
The alpha byte runs
00ff8050rgba()RGB: the same numbers, in decimal
rgb()css.card { color: rgb(26 115 232); /* same as #1a73e8 */ background: rgb(255 255 255); /* white */ }
Note the modern space-separated syntax. The older comma form —
rgb(26, 115, 232)rgb(10% 45% 91%)Alpha in RGB
Alpha goes after a slash in the modern syntax, or as a fourth comma-separated value in
rgba()css.shadow-layer { background: rgb(0 0 0 / 0.5); /* black at 50% — readable! */ border: 1px solid rgba(0, 0, 0, 0.12); }
Here alpha is a 0–1 float (or a percentage), so 50% is genuinely
0.5rgb(... / α)rgb()rgba()rgba()HSL: the format you can actually reason about
HSL describes color the way a person would: Hue, Saturation, Lightness.
- Hue is an angle on the color wheel, 0–360 degrees. 0/360 is red, 120 is green, 240 is blue, and the rest fall in between (60 yellow, 180 cyan, 300 magenta).
- Saturation is how vivid the color is, 0% (gray) to 100% (full intensity).
- Lightness is how bright it is, 0% (black) to 100% (white), with 50% being the "pure" color.
css.brand { color: hsl(262 83% 58%); /* a vivid purple */ background: hsl(0 0% 100%); /* white: any hue, 0 saturation, full lightness */ }
Alpha works the same way as
rgb()css.brand-tint { background: hsl(262 83% 58% / 0.15); }
Why HSL is easier to reason about
The value of HSL is that its three dials map onto things you actually want to change.
Building a tonal scale. Keep hue and saturation fixed, walk lightness up and down, and you get a coherent set of shades — exactly what design systems need:
css:root { --primary-100: hsl(262 83% 96%); --primary-300: hsl(262 83% 80%); --primary-500: hsl(262 83% 58%); /* base */ --primary-700: hsl(262 83% 40%); --primary-900: hsl(262 83% 24%); }
Doing this in HEX means hand-picking five unrelated-looking values. In HSL it's one number moving.
Hover and active states. Darken a button on hover by dropping lightness a few points — no separate color needed:
css.btn { background: hsl(262 83% 58%); } .btn:hover { background: hsl(262 83% 50%); } .btn:active { background: hsl(262 83% 42%); }
Theming. Store hue and saturation as variables and let lightness flip for dark mode, or rotate the hue to re-skin an entire UI from a single token.
HSL has a known limitation: perceived brightness isn't uniform across hues.
hsl(60 100% 50%)hsl(240 100% 50%)hsloklch()Converting between the formats
HEX ↔ RGB
This one is pure base conversion, no color theory involved. Each hex pair is one decimal channel:
- →text
#1a73e8= 1×16 + 10 = 26,text1a= 7×16 + 3 = 115,text73= 14×16 + 8 = 232 →texte8textrgb(26 115 232) - Going back, divide by 16 for the first digit and take the remainder for the second.
In JavaScript:
jsconst hexToRgb = (hex) => { const n = parseInt(hex.replace('#', ''), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; }; const rgbToHex = (r, g, b) => '#' + [r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('');
RGB ↔ HSL
This conversion involves real geometry. To go from RGB to HSL you normalize the channels to 0–1, find the max and min, and derive lightness from their average, saturation from their spread, and hue from which channel is largest:
jsfunction rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); const l = (max + min) / 2; let h = 0, s = 0; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)); else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4; h *= 60; } return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; }
The reverse (HSL → RGB) walks the color wheel back into channel intensities. It's a dozen lines of math you rarely want to write by hand, and it's where rounding errors creep in — a HEX → HSL → HEX round trip can land one unit off on a channel.
In practice you almost never do this manually. The browser does it for you whenever you assign any format to a property, and for one-off conversions a tool is faster and less error-prone than re-deriving the formula. Our color converter turns any HEX, RGB, or HSL value into the other two instantly, and the color picker lets you drag hue, saturation, and lightness and read off every format at once — useful when you want to see what a hue rotation or lightness shift does before committing it to a stylesheet. Both run entirely in your browser.
Which one should you use?
There's no single right answer, but a reasonable default:
- HEX for fixed brand colors and anywhere you're copying values between tools — it's the universal interchange format.
- when you need a specific opacity, especially for overlays, scrims, and hairline borders, because alpha reads as a plain 0–1 number.text
rgb(... / α) - HSL for anything you'll adjust programmatically — tonal scales, hover states, theming, and design tokens — because its dials match the changes you actually make.
Because they're all just sRGB underneath, you can mix them freely in one stylesheet. Reach for the format that makes the specific line of CSS easiest to read and to change later — that's the whole game.