One colour, five notations, and why they exist
Every notation on this page describes the same physical thing: how much red, green and blue light a display emits. They differ only in how they index that space, and each was designed for a different job.
Hex is RGB written in base 16, two digits per channel, because a byte fits in exactly two hex digits. #3B82F6 means red 0x3B = 59, green 0x82 = 130, blue 0xF6 = 246. Shorthand triples the way you would expect but not the way most people guess: #0f0 expands to #00FF00 by repeating each digit, so f becomes ff = 255, not f0 = 240.
RGB is the same three numbers on a 0–255 scale, which is what image editors and CSS both accept directly.
HSL re-indexes the cube as a cylinder: a hue angle, a saturation, and a lightness. It exists because “make this 10% darker but the same colour” is one subtraction in HSL and three coupled changes in RGB. That is why design systems store palettes as HSL ramps.
HSV (also called HSB) uses the same hue but replaces lightness with value, the largest channel. In HSV, full saturation at full value gives the pure hue; in HSL the pure hue sits at 50% lightness. The two saturations are not interchangeable, which is the single most common source of “the colour picker disagrees with my code”.
CMYK is subtractive, meant for ink on paper rather than light from a panel. The conversion here is the naive one — pull out the common black, express what remains as ink percentages — and it is genuinely useful for rough proofs and nothing else.
Underneath all of them is the sRGB colour space defined in IEC 61966-2-1, which fixes what “255 red” actually means in terms of light. The relative luminance figure is where that definition becomes visible, and it is the number the CSS Color Module and the accessibility guidelines both build on.
The arithmetic, step by step
Start by normalising: divide each channel by 255 so R, G and B all live in 0–1. Then take max and min of the three and their difference Δ. Almost everything else falls out of those three numbers.
Lightness is the midpoint of the extremes: L = (max + min) / 2. A colour with channels 59, 130, 246 has max 0.9647 and min 0.2314, so L = 0.5980, or 59.8%.
Saturation asks how wide the spread is relative to the widest spread possible at that lightness. The widest possible Δ is 1 − |2L − 1|, which peaks at 1 when L = 0.5 and shrinks to 0 at pure black or pure white. So S = Δ / (1 − |2L − 1|). When Δ is zero the colour is grey and S is defined as 0 — the division is skipped, not attempted, because the denominator also collapses at the extremes.
Hue is a sector formula keyed on which channel is largest. If red is the maximum, H = 60 × ((G − B) / Δ), wrapped into 0–360 by adding 360 if it comes out negative. If green is the maximum, H = 60 × ((B − R) / Δ + 2). If blue is the maximum, H = 60 × ((R − G) / Δ + 4). The +2 and +4 rotate you onto the right 120° arc of the wheel; the inner fraction places you inside it.
HSV shares that hue exactly, but sets V = max and S_hsv = Δ / max. Because the denominator changed, the two saturation numbers differ for the same colour — 59/130/246 is 91% saturated in HSL and 76% in HSV.
CMYK takes the black out first: K = 1 − max. Whatever is left is scaled back up, C = (1 − R − K) / (1 − K) and likewise for M and Y. Pure black has K = 1 and the division is skipped, since there is no remaining colour to express.
Relative luminance is the one calculation that is not a re-indexing. Screen values are gamma-encoded, so you must linearise each channel before weighting it: c_lin = c/12.92 when c ≤ 0.03928, otherwise ((c + 0.055)/1.055)^2.4. Then Y = 0.2126·R + 0.7152·G + 0.0722·B. Those weights reflect how sensitive human vision is to each primary, which is why green carries over seven times the weight of blue.
Worked example: converting #3B82F6 by hand
Take the hex value #3B82F6 and derive every other notation.
- Hex to decimal. 0x3B = 3×16 + 11 = 59. 0x82 = 8×16 + 2 = 130. 0xF6 = 15×16 + 6 = 246. So the colour is
rgb(59, 130, 246). - Normalise. R = 59/255 = 0.2314, G = 130/255 = 0.5098, B = 246/255 = 0.9647.
- Max, min and Δ. max = 0.9647 (blue), min = 0.2314 (red), Δ = 0.7333.
- Lightness. L = (0.9647 + 0.2314) / 2 = 0.5980 → 60%.
- Saturation (HSL). 1 − |2(0.5980) − 1| = 1 − 0.1961 = 0.8039. S = 0.7333 / 0.8039 = 0.9122 → 91%.
- Hue. Blue is the maximum, so H = 60 × ((R − G)/Δ + 4) = 60 × ((0.2314 − 0.5098)/0.7333 + 4) = 60 × (−0.3797 + 4) = 60 × 3.6203 = 217.2° → 217°. The CSS value is
hsl(217, 91%, 60%). - HSV. V = max = 0.9647 → 96%. S_hsv = 0.7333 / 0.9647 = 0.7602 → 76%. So 217°, 76%, 96% — note the saturation differs from HSL's 91%.
- CMYK. K = 1 − 0.9647 = 0.0353 → 4%. C = (1 − 0.2314 − 0.0353) / 0.9647 = 0.9333 / 0.9647 = 0.9675 → 97%. M = (1 − 0.5098 − 0.0353) / 0.9647 = 0.4549 / 0.9647 = 0.4715 → 47%. Y = (1 − 0.9647 − 0.0353) / 0.9647 = 0%. So 97%, 47%, 0%, 4%.
- Relative luminance. Linearise: R_lin = ((0.2314 + 0.055)/1.055)^2.4 = 0.0437; G_lin = ((0.5098 + 0.055)/1.055)^2.4 = 0.2232; B_lin = ((0.9647 + 0.055)/1.055)^2.4 = 0.9216. Then Y = 0.2126×0.0437 + 0.7152×0.2232 + 0.0722×0.9216 = 0.0093 + 0.1596 + 0.0665 = 0.2355.
That last figure is the one you carry into an accessibility check: against white (Y = 1.0) the contrast ratio is (1.0 + 0.05) / (0.2355 + 0.05) = 3.68:1, which clears the 3:1 bar for large text but not the 4.5:1 bar for body copy.
How to read the numbers you get back
Use hue to judge whether two colours belong to the same family. Hues within roughly 15° of each other read as the same colour at a glance; a 180° difference is the complement. Because hue is an angle, 5° and 355° are neighbours, not opposites — a fact that breaks naive “sort the palette by hue” code.
Use lightness rather than value when you are building a ramp. An HSL ramp at fixed hue and saturation with lightness stepping 90%, 80%, 70% and so on gives predictable tints and shades. An HSV ramp does not, because dropping value darkens while dropping saturation washes out, and the two axes are not symmetric about a midpoint the way HSL's lightness is.
Treat the luminance figure as the only perceptually meaningful brightness number on the page. HSL lightness of 50% does not mean “half as bright as white”: hsl(60, 100%, 50%) (yellow) has a relative luminance of 0.9278 while hsl(240, 100%, 50%) (blue) has 0.0722, and both claim 50% lightness. Yellow really is over twelve times as luminous as blue at the same nominal lightness. If you are choosing text and background colours, work from luminance, and let the WCAG contrast ratio calculator apply the thresholds.
Treat the CMYK figures as indicative only. Real separations depend on the paper, the press, the total ink limit and an ICC profile; the naive formula knows none of that. It is fine for “roughly how much cyan is in this”, and wrong for anything that will be printed and paid for.
Reference colours in every notation
| Colour | HEX | RGB | HSL | CMYK | Luminance |
|---|---|---|---|---|---|
| White | #FFFFFF | 255, 255, 255 | 0°, 0%, 100% | 0, 0, 0, 0 | 1.0000 |
| Black | #000000 | 0, 0, 0 | 0°, 0%, 0% | 0, 0, 0, 100 | 0.0000 |
| Mid grey | #808080 | 128, 128, 128 | 0°, 0%, 50% | 0, 0, 0, 50 | 0.2159 |
| Red | #FF0000 | 255, 0, 0 | 0°, 100%, 50% | 0, 100, 100, 0 | 0.2126 |
| Green | #00FF00 | 0, 255, 0 | 120°, 100%, 50% | 100, 0, 100, 0 | 0.7152 |
| Blue | #0000FF | 0, 0, 255 | 240°, 100%, 50% | 100, 100, 0, 0 | 0.0722 |
| Yellow | #FFFF00 | 255, 255, 0 | 60°, 100%, 50% | 0, 0, 100, 0 | 0.9278 |
| Cyan | #00FFFF | 0, 255, 255 | 180°, 100%, 50% | 100, 0, 0, 0 | 0.7874 |
| Magenta | #FF00FF | 255, 0, 255 | 300°, 100%, 50% | 0, 100, 0, 0 | 0.2848 |
Luminance values are the weighted sums of the linearised channels: a primary at full strength contributes exactly its coefficient, so yellow is 0.2126 + 0.7152 = 0.9278 and magenta is 0.2126 + 0.0722 = 0.2848.
Traps in colour conversion
- Expanding shorthand hex by padding with zeros.
#0f0is#00FF00, not#00F000. Each digit is repeated, which keeps 0 at 0 and f at 255 so the endpoints of the scale stay put. - Swapping HSL and HSV saturation. They use different denominators and disagree for every colour that is not fully saturated or fully grey. Design apps often show HSB while CSS speaks HSL.
- Treating HSL lightness as brightness. Yellow and blue both sit at 50% lightness and differ by a factor of nearly thirteen in actual luminance.
- Rounding HSL and expecting to get the same hex back. Only 16.7 million colours exist in 8-bit RGB, and the HSL cylinder is continuous, so a rounded
hsl()string usually maps to a neighbouring hex value. - Sending naive CMYK to a printer. Without an ICC profile the numbers ignore ink limits, paper white and dot gain. Ask the printer for their profile and separate through it.
- Assuming hex means sRGB everywhere. On a wide-gamut display,
#FF0000is whatever the display's red primary is unless the content is colour-managed. IEC 61966-2-1 defines sRGB specifically so that assumption is safe on standard hardware. - Forgetting alpha is not a colour. Compositing
rgba(0,0,0,0.5)over white gives a different final luminance than#808080, because alpha blending happens in the encoded space that browsers use, not in your source value.
Where this fits, and when to reach for something else
Colour conversion is the first step in most front-end colour work and the last step in most design-handoff work. Once you have a hex value you trust, the accessibility question follows immediately, and the contrast ratio calculator takes two of these values and reports pass or fail against the WCAG thresholds. If the colour is going into an image asset, the image file size calculator shows what bit depth and palette choices cost you in bytes, and the aspect ratio calculator handles the dimensions.
Use a different tool when perceptual uniformity matters. HSL and HSV are cheap cylindrical re-indexings of the RGB cube and are not perceptually uniform: a 10° hue step near green is a much smaller visual change than the same step near blue. For palette generation, gradient interpolation or measuring colour difference, work in CIELAB, OKLab or OKLCH instead — the CSS Color Module Level 4 exposes all three, and they are designed so that equal numeric steps look like equal visual steps.
Use a colour-managed pipeline when the destination is print or a wide-gamut display. Naive CMYK and unqualified hex both assume sRGB; the moment you leave that assumption, only an ICC profile gets you the right answer. And when you are embedding colour data in a payload rather than a stylesheet, the Base64 size calculator tells you what encoding an inline image costs in transfer bytes.
