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,

text
rgb()
, and
text
hsl()
are all just different ways to name a point inside it.

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 (

text
00
to
text
ff
, i.e. 0 to 255):

css
.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 (

text
#000
,
text
#f0c
) expands by doubling each digit —
text
#f0c
means
text
#ff00cc
, not
text
#f00c
. It only works when both digits of each channel are identical, which is why you can't shorten arbitrary colors.

HEX 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:

text
#7c3aed
tells you nothing about whether it's light or dark, warm or cool, or how to make it slightly more saturated. You have to run it through your head as three base-16 numbers.

Alpha 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

text
00
(fully transparent) to
text
ff
(fully opaque). The catch is the same opacity-as-base-16 mental math: 50% is
text
80
, not
text
50
. This is supported in all current browsers, but if you need to support very old ones, prefer
text
rgba()
.

RGB: the same numbers, in decimal

text
rgb()
writes those same three channels as decimal integers, which is easier to reason about than hex for most people:

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 —

text
rgb(26, 115, 232)
— still works everywhere and you'll see it constantly. CSS Color 4 also allows the channels to be percentages (
text
rgb(10% 45% 91%)
) and even decimals, which is handy when a value comes out of a calculation.

Alpha in RGB

Alpha goes after a slash in the modern syntax, or as a fourth comma-separated value in

text
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

text
0.5
. For overlays, scrims, and subtle borders,
text
rgb(... / α)
is usually clearer than the equivalent 8-digit HEX. Functionally,
text
rgb()
with alpha and
text
rgba()
are identical now — the separate
text
rgba()
function is kept for backward compatibility.

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

text
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.

text
hsl(60 100% 50%)
(yellow) looks far brighter than
text
hsl(240 100% 50%)
(blue) at the same lightness value. For perceptually even scales, CSS Color 4 adds
text
hsl
's better-behaved cousins like
text
oklch()
. But for day-to-day work, HSL is intuitive, universally supported, and good enough — and it's a much better thing to think in than HEX.

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
    text
    1a
    = 1×16 + 10 = 26,
    text
    73
    = 7×16 + 3 = 115,
    text
    e8
    = 14×16 + 8 = 232
    text
    rgb(26 115 232)
  • Going back, divide by 16 for the first digit and take the remainder for the second.

In JavaScript:

js
const 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:

js
function 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.
  • text
    rgb(... / α)
    when you need a specific opacity, especially for overlays, scrims, and hairline borders, because alpha reads as a plain 0–1 number.
  • 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.