ENES
colorEngineering Guide

Hex, RGB, and HSL: The Math Behind Color Conversion (2026)

AS
Published on 2026-10-19ยท8 min readยทDaily Toolbox Engineering

Hex, RGB, and HSL: The Math Behind Color Conversion

Every color picker, CSS file, and design tool juggles three representations of the same thing: hex (#FF5733), RGB (rgb(255, 87, 51)), and HSL (hsl(11, 100%, 60%)). Converting between them is pure arithmetic โ€” no magic, no lookup tables. But the formulas have sharp edges (achromatic edge cases, hue wraparound, gamma), and naive conversions produce colors that look wrong. Here is the math, with examples you can verify by hand.

Hex: RGB in Base 16

A 6-digit hex color is just three bytes written in hexadecimal: #RRGGBB. Each pair is one channel (red, green, blue) in the range 0x00โ€“0xFF (0โ€“255).

#FF5733 breaks down as R = 0xFF = 255, G = 0x57 = 87, B = 0x33 = 51.

The 3-digit shorthand #F53 expands by doubling each digit: #FF5533 (note: it is #FF5533, not #F05030 โ€” each digit is duplicated). 8-digit hex adds an alpha channel: #FF573380 โ€” the last byte 0x80 = 128, about 50% opacity.

Conversion is trivially reversible:

def hex_to_rgb(h):
    h = h.lstrip('#')
    if len(h) == 3:
        h = ''.join(c * 2 for c in h)
    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

def rgb_to_hex(r, g, b):
    return f'#{r:02X}{g:02X}{b:02X}'

hex_to_rgb('#FF5733')   # (255, 87, 51)
rgb_to_hex(255, 87, 51) # '#FF5733'

RGB: The Machine Model

RGB describes color as three light intensities. It is how displays work (additive color: red + green + blue light = white) and how canvas, WebGL, and image buffers store pixels. But RGB is not perceptually uniform: numerically halfway between two RGB colors is rarely visually halfway. It is also terrible for human manipulation โ€” "make this slightly less saturated" has no simple expression in RGB.

That is why HSL exists.

HSL: The Human Model

HSL remaps the same color into three intuitive dimensions:

  • Hue (H): the angle on the color wheel, 0โ€“360 degrees. 0 = red, 120 = green, 240 = blue.
  • Saturation (S): 0โ€“100%. 0% = gray, 100% = the purest form of the hue.
  • Lightness (L): 0โ€“100%. 0% = black, 100% = white, 50% = the "normal" color.

HSL makes programmatic color work pleasant: hover states (lightness minus 10%), palettes (fixed S and L, rotate H by 30 degrees), disabled states (saturation 0%).

RGB to HSL: The Algorithm

Given R, G, B in the range 0โ€“1 (divide 0โ€“255 values by 255):

  1. mx = max(R, G, B), mn = min(R, G, B), d = mx - mn.
  2. Lightness: L = (mx + mn) / 2.
  3. Saturation: if d == 0 then S = 0 (achromatic โ€” a shade of gray; hue is undefined, conventionally 0). Otherwise S = d / (1 - abs(2*L - 1)).
  4. Hue: if d == 0 then H = 0. If mx == R: H = 60 * (((G - B) / d) mod 6). If mx == G: H = 60 * ((B - R) / d + 2). If mx == B: H = 60 * ((R - G) / d + 4).

Worked Example: #FF5733

Normalize: R = 1.0, G = 87/255 โ‰ˆ 0.341, B = 51/255 = 0.2.

  • mx = 1.0, mn = 0.2, d = 0.8.
  • L = (1.0 + 0.2) / 2 = 0.6 โ†’ 60%.
  • S = 0.8 / (1 - abs(1.2 - 1)) = 0.8 / 0.8 = 1.0 โ†’ 100%.
  • mx == R: H = 60 * (((0.341 - 0.2) / 0.8) mod 6) = 60 * 0.176 โ‰ˆ 10.6 โ†’ about 11 degrees.

Result: hsl(11, 100%, 60%) โ€” a vivid orange-red, matching what you see on screen.

HSL to RGB: The Algorithm

Given H in 0โ€“360, S and L in 0โ€“1:

  1. C = (1 - abs(2*L - 1)) * S (chroma).
  2. X = C * (1 - abs((H / 60) mod 2 - 1)).
  3. m = L - C / 2.
  4. Pick (Rp, Gp, Bp) by hue sector: 0โ€“60 โ†’ (C, X, 0); 60โ€“120 โ†’ (X, C, 0); 120โ€“180 โ†’ (0, C, X); 180โ€“240 โ†’ (0, X, C); 240โ€“300 โ†’ (X, 0, C); 300โ€“360 โ†’ (C, 0, X).
  5. Add m to each component and multiply by 255.

Verify with our example: H = 10.6, S = 1.0, L = 0.6. Then C = 0.8; X = 0.8 * (1 - abs(10.6/60 mod 2 - 1)) = 0.8 * (1 - 0.823) โ‰ˆ 0.141; m = 0.2. Sector 0โ€“60 gives (0.8, 0.141, 0); add m โ†’ (1.0, 0.341, 0.2); times 255 โ†’ (255, 87, 51). The round trip lands exactly on the original RGB.

Why Naive Brightness Math Lies

"Darken by 20%" in RGB usually means multiplying each channel by 0.8. Two problems:

  1. Gamma. Displays do not map 0โ€“255 linearly to light output โ€” they apply a gamma curve near 2.2. Channel value 128 is not half the light of 255; it is roughly 22%. Multiplying sRGB channels directly darkens perceptually more than the number suggests. The correct approach: convert to linear light, scale, convert back.
  2. Hue shift. Scaling RGB channels toward black is safe, but lightening by adding a constant shifts hue โ€” #804000 plus 60 per channel is #BC7C3C, which reads as a different, washed-out color rather than "the same orange, lighter." In HSL, adjusting lightness preserves hue by construction.

For UI work, do brightness and saturation adjustments in HSL (or better, Oklch โ€” see FAQ), and convert to RGB or hex only at the end.

Common Mistakes

1. Treating hue as linear. Hue is circular: 359ยฐ and 1ยฐ are 2ยฐ apart, not 358ยฐ apart. Averaging hues with the arithmetic mean gives garbage (the average of 350ยฐ and 10ยฐ is 180ยฐ โ€” cyan โ€” instead of 0ยฐ red). Average via vector math: convert to (cos H, sin H), average the vectors, then atan2 back.

2. Forgetting the achromatic case. When R = G = B, d = 0 and the hue/saturation formulas divide by zero. Handle it explicitly: S = 0, H = 0 (or "undefined").

3. Rounding too early. Round only the final 0โ€“255 integers. Rounding intermediate floats accumulates error โ€” especially visible in gradients built by repeated conversion.

4. Assuming hex is always 6 digits. CSS accepts 3, 4, 6, and 8-digit hex (the 3/4-digit forms are shorthand; the 8/4-digit forms include alpha). Parsers must handle all four.

5. Ignoring color spaces. rgb(255, 0, 0) in sRGB vs Display-P3 are different physical colors. CSS Color 4 lets you write color(display-p3 1 0 0). Converting between color spaces needs ICC-style profiles or the CSS-defined matrices โ€” plain HSL math assumes sRGB throughout.

Debugging Walkthrough: "The Gradient Has an Ugly Gray Band"

  1. Check the interpolation space. Interpolating red to green in RGB passes through muddy brown-gray (rgb(128, 128, 0)); in HSL it sweeps through yellow โ€” usually what is wanted. If a gradient looks dirty, the interpolation is happening in the wrong space.
  2. Look for the achromatic trap. If one endpoint has S = 0, its hue is meaningless (conventionally 0ยฐ) โ€” interpolating from that hue swings across the whole wheel. Fix: interpolate hue along the shorter arc, or hold the colorful endpoint's hue.
  3. Verify gamma handling. Banding in dark gradients often means interpolation happened in gamma-encoded sRGB instead of linear light. Convert to linear, interpolate, convert back.
  4. Count the bits. 8-bit-per-channel gradients over a wide range show visible steps (banding). Dithering or 10-bit output fixes it โ€” the math was right, the quantization was too coarse.

Try It

Color math is easier with instant feedback. DailyToolbox's free color picker runs entirely in your browser โ€” convert between hex, RGB, and HSL, build palettes, and check contrast ratios without sending anything anywhere.

๐Ÿ‘‰ Pick a color โ†’

FAQ

What is Oklch and should I use it instead of HSL?

Oklch is a newer color space designed to be perceptually uniform โ€” equal numeric steps look like equal visual steps, which HSL does not guarantee (HSL's "50% lightness" blue looks much darker than 50% lightness yellow). CSS supports oklch() in all modern browsers. For design-system work it is the better choice; HSL remains fine for simple programmatic tweaks.

Why do the same hex values look different on my phone vs my monitor?

Different displays have different color gamuts, white points, and calibration. #FF5733 is a coordinate in the sRGB color space, not a physical light recipe โ€” each screen interprets it through its own characteristics. Color-managed workflows (ICC profiles) narrow the gap but never fully close it.

How do I pick accessible text and background combinations?

Use the WCAG contrast ratio: 4.5:1 for normal text, 3:1 for large text. The ratio is computed from relative luminance, which weights green heavily because human eyes are most sensitive to green. Do not eyeball it โ€” compute it. Many color pickers (including the linked one) show the ratio live.

Can I convert CMYK to RGB exactly?

No. CMYK is subtractive (ink on paper) and device-dependent โ€” the same CMYK values produce different colors on different printers and papers. Conversion requires an ICC profile for the specific printer and paper combination. Any "CMYK to RGB" tool without a profile is making an approximation.

Why does hsl(0, 100%, 50%) equal pure red?

At full saturation and 50% lightness, HSL produces the most vivid version of the hue. For hue 0 (red) that is rgb(255, 0, 0). It is a useful sanity check: if your converter does not map hsl(0, 100%, 50%) to #FF0000, the implementation has a bug.

#color#css#hex#rgb#hsl#web-development
AS
Written by Alex Sun

Alex Sun is the developer behind Daily Toolbox. He writes these guides while building the tools themselves โ€” every claim tested against the real thing.

Try the free tools mentioned above

Try the Color Picker โ†’