Notes › Resources › Converting between HEX, RGB and HSL: colour picking and comparison

Converting between HEX, RGB and HSL: colour picking and comparison

While adding dark mode to the company's component library, I inherited a color spec that listed HEX values only. The designer asked for a hover state that is just a little darker than the primary color. It sounds like half a sentence of work, but I kept failing to line up that blue: converting #1E90FF to HSL by hand, I calculated saturation as 88%, then changed the lightness, converted back to HEX, and had a different color. The same button was one step off between the design file and the web page, and review went back and forth for two days treating it as a fidelity problem. The formats themselves are not hard; what is hard is the few steps where human hands are most likely to slip while moving between the three notations.

1. The three notations describe the same thing on different axes

HEX and RGB are two ways of writing the same red, green and blue channels, while HSL swaps in a set of axes that matches intuition better: hue, saturation, lightness. Here is the same blue in four notations:

FormatSyntaxValue for this blueComponent rangeWhen to use it
HEX#RRGGBB#1E90FFEach pair 00–FFDesign annotations, CSS variables, brand color documents
RGBrgb(r,g,b)rgb(30,144,255)Each channel 0–255When the value goes into computation or JavaScript
HSLhsl(h,s%,l%)hsl(210,100%,56%)H 0–360, S/L 0–100%Deriving shades, building a theme scale
RGBArgba(r,g,b,a)rgba(30,144,255,0.5)A from 0 to 1Overlays, translucent hover states

This is why HSL is easier inside a theme system: change only L, leave H and S alone, and you get another step of the same hue. Push that blue's lightness from 56% down to 46% and you get about rgb(0,117,235), close to a darker #0075EB, which is exactly right for a hover state.

2. Why hand calculation goes wrong: the intermediate values both formulas need

HEX to RGB is mechanical; look up each pair of hex digits: 1E=30, 90=144, FF=255. The trouble starts with RGB to HSL, which first needs the maximum and minimum:

  • Lightness L = (max + min) / 2. For that blue, max=255 and min=30, so (255+30)/2/255 is about 56%.
  • Saturation S = (max − min) / (1 − |2L − 1|), and the |2L−1| in the denominator is the term everyone forgets. I originally used (max−min)/max, got 88%, while the correct value is 100%.
  • Hue requires picking a formula from three cases depending on which channel is largest, and the result can come out negative, so you add 360 and take the modulus. This step is almost guaranteed to go wrong by hand.

Going the other direction, HSL back to HEX, passes through chroma and more intermediate values, and converting both ways once means a dozen multiplications and divisions. With color, hand calculation is a terrible trade. These days I type any one format into Color Converter and get all three values at once, then copy the one I need.

3. The worst part with transparency: byte order in 8-digit HEX

In CSS, 8-digit hex is #RRGGBBAA, with alpha last: rgba(30,144,255,0.5) becomes #1E90FF80 (0.5 x 255 is about 128, which is 80). But Android color integers, a number of design plugins and several image formats use #AARRGGBB, with alpha first. Move the same value to a different position and it reads as an entirely different color: parse #1E90FF80 as #AARRGGBB and alpha is taken from 0x1E, only about 12%, while the other three bytes read as rgb(144,255,128). What should have been a translucent blue overlay ends up nearly invisible pale green. That overlay of mine turned strange colors all over iOS because of this order. When passing color values between platforms, write an extra comment naming the channel order.

4. Once the value is fixed, check that it stays legible on its background

WCAG asks for a contrast ratio of 4.5:1 between body text and its background, relaxed to 3:1 for large text. Put #1E90FF on pure white and the relative luminance is about 0.27, giving a ratio of about 3.2:1, which actually fails for small text on white. Switch to #0075EB, one step darker, and the ratio rises to about 4.4:1, still a hair short, so L has to come down another notch or two. This kind of judgment needs no instrument; what it needs is not mistaking good-looking for legible.

Two more stages quietly change color values. The first is where you sample: when picking a color from a screenshot, an image that carries an ICC profile or has been compressed will read 1–3 units off the design file. The second is the display: on an ordinary office monitor with incomplete sRGB coverage, saturated greens and blues look noticeably gray. When I write a spec, I prefer to fix HEX as the source of truth and use HSL only as a derivation tool.

5. Common questions

Q: What is a three-digit value like #fff?

It is shorthand for #ffffff, and the rule is that every digit must be exactly doubled. #1E90FF cannot be shortened; #FF00AA can be written as #F0A.

Q: Why does H in HSL run to 360 and come back to red?

Because hue is a circle: 0 red, 60 yellow, 120 green, 180 cyan, 240 blue, 300 magenta, with 360 landing on 0. For same-hue derivations, add or subtract ten-odd degrees to get a neighboring color, or add 180 degrees for the complement.

Q: What happens when L goes to 0 and 100?

L=0 is pure black and L=100 is pure white, and at that point S and H no longer affect the result. So brightening a color past 90% starts to lose the hue, and text colors in dark mode usually sit between 70% and 90%.

Q: Should theme variables store HEX or HSL?

It depends on which dimension you change. If you only shift lightness and build light and dark themes, HSL is easier to compute with. If you only swap the brand color wholesale and need to match the design file one to one, HEX is more direct. The two convert losslessly, so there is no worry about changing back later.

Q: Why does a color sampled from a screenshot not match the spec?

The screenshot pipeline applies color management and compression, so the sampled value can be 1–3 off. When you need an exact value, take it from the annotation in the design file or from the CSS variable itself, then convert it to the format you need.

The spec for that component library now has two layers: HEX is the single authoritative value, and HSL is used only to generate state colors, with one line of the converted result attached in the doc. Those two days of back and forth came down to handing a vague phrase like "a little darker" to two coordinate systems at once. Once conversion became a fixed input-and-output step, that kind of disagreement no longer needs a meeting.

📎 Related articles

SOS is 9 tones, the full alphabet is 104: how Morse timing is calculated 2026-10-21 What does a weight of 30 mean on a spinner? Sector angles and draws with or without replacement 2026-10-21 Why white text on gradients is hard to read: checking WCAG contrast at every stop 2026-10-21 Enlarging old photos without blur: how much detail enhancement can recover 2026-10-15 Barcodes for inventory labels: choosing between CODE128 and EAN-13 2026-10-15