Three descriptions of one colour
A colour on the web is a triple of red, green and blue intensities in the sRGB space. HEX is that triple written in base 16, RGB is the same triple in decimal, and HSL is a re-parameterisation: hue, saturation and lightness. None of them is more accurate than another; they are coordinates, and converting between them is arithmetic, not interpretation.
| Notation | Example | Reads as |
|---|---|---|
| HEX | #4682B4 | Three byte pairs: 46, 82, B4 |
| RGB | rgb(70, 130, 180) | Decimal equivalents of the same bytes |
| HSL | hsl(210, 51%, 49%) | A rotation around the colour wheel, not a new value |
Round-tripping through HSL loses information because HSL is usually displayed with integer degrees and percentages. A colour converted to HSL and back can shift by one in a channel; the HEX and RGB forms are exact, so copy those when the value has to be preserved. This is not a bug in a converter — it is the precision of the description you chose.
HSL is convenient, not perceptual
HSL is useful because “make it lighter” is a single slider and “rotate the hue” is a single number. It is not perceptually uniform: a 50% lightness in HSL is not a mid-perceived brightness, and two colours with the same saturation can look wildly different in intensity — a saturated yellow and a saturated blue at the same HSL lightness do not have the same apparent brightness. A palette generated by rotating hue keeps a mathematical relationship, not a visual one.
For perceived brightness, use relative luminance, the quantity WCAG defines for contrast. It weights the channels by their contribution to human vision (roughly 0.2126 R, 0.7152 G, 0.0722 B after linearising each channel), which is why green contributes far more than blue. That weighting is the reason a dark blue on black can look almost invisible even when its HSL lightness is not zero.
The contrast ratio, measured
The ratio is (L1 + 0.05) ÷ (L2 + 0.05), where L1 is the lighter relative luminance and L2 the darker. It ranges from 1:1 (identical) to 21:1 (black on white). Measured with the same function the contrast checker uses:
| Foreground | Background | Ratio | WCAG normal text |
|---|---|---|---|
| #000000 | #FFFFFF | 21.00:1 | AAA |
| #767676 | #FFFFFF | 4.54:1 | AA (pass) |
| #777777 | #FFFFFF | 4.48:1 | AA (fail, just) |
| #4682B4 | #FFFFFF | 4.11:1 | AA (fail for normal text) |
The #767676 / #777777 pair is the useful one: one hexadecimal step is the difference between passing and failing AA for normal text. Contrast is a threshold, not a gradient, and “close” is a fail.
The thresholds are 4.5:1 for normal text, 3:1 for large text (18 pt, or 14 pt bold) and 3:1 for non-text elements such as icons and focus outlines. AAA raises normal text to 7:1. These are minimums for accessibility, not a definition of good design; a page can pass AA and still be uncomfortable to read if the type is small or the display is dim.
Palettes do not check themselves
A palette extractor finds colours that are present in an image. It does not know which pairs will be used as text and background, and it cannot guarantee that any pair passes contrast. Extracting five colours from a photograph and using them as a scheme is a starting point; the next step is to test the specific foreground/background pair you will ship, including the hover and disabled states, which are the pairs most often forgotten.
The 3:1 non-text threshold is the one most often skipped. A focus ring, a checkbox border, an icon that carries meaning and a chart line all need to be distinguishable from their background, not just the text. A light grey outline that looks subtle on a designer's monitor can disappear entirely on a laptop in daylight, and the ratio is the only way to know before shipping.
Alpha is the other trap. A colour with transparency has no single contrast ratio because the result depends on what is behind it. Compositing a 50% black text over an image can pass in one area and fail in another; either composite the final colours before measuring, or use an opaque background behind the text.
Gamma, profiles and alpha
The channel values in HEX and RGB are gamma-encoded sRGB, not linear light. WCAG's relative luminance linearises each channel before weighting it, which is why contrast is not proportional to the numeric distance between two colours: #777777 and #767676 differ by one in every channel, and that one step is the difference between 4.48:1 and 4.54:1. Two colours that look equally far apart in HEX can have very different luminance ratios.
Colour profiles add another layer. A value tagged with Display P3 describes a wider gamut than sRGB; the same numbers rendered without the profile look duller. Alpha adds a third: a translucent colour has no single contrast ratio because the result depends on what is composited behind it. For a contrast check, composite the final colours first and measure the opaque result.
A short workflow
- Convert the colour to HEX or RGB and keep that as the source of truth; treat HSL as a view.
- Choose the text/background pair, then measure the ratio for that exact pair, not for the palette as a whole.
- If it fails, adjust lightness rather than hue first — it changes luminance most directly — and re-measure.
- Check the states: hover, focus, disabled, and text over images. A contrast checker tests one pair at a time.
- For non-text UI, remember the 3:1 threshold applies to icons and focus rings too.