DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Color Spaces for Developers: Why Your Eyes Disagree With the Math

Try something. Open any design tool and create two colors: #FF0000 (pure red) and #00FF00 (pure green). Now look at them side by side. The green looks significantly brighter than the red, even though both have the exact same maximum value of 255 in their respective channels. The math says they're equal. Your eyes say otherwise.

This is because human vision is not equally sensitive to all wavelengths of light. We perceive green as roughly twice as bright as red and nearly six times as bright as blue. The RGB color model doesn't account for this. It's a model of how screens emit light, not how humans perceive it. Understanding this disconnect is the key to making better color decisions in code.

RGB is a hardware model

RGB (Red, Green, Blue) maps directly to the three types of subpixels in your display. Each channel gets a value from 0 to 255 (in 8-bit color), and the screen mixes them additively. It's straightforward and it's how browsers render every color on screen.

But "straightforward" doesn't mean "intuitive." In RGB, making a color darker means reducing all three channels. Making it lighter means increasing them. Want a muted version of a vivid blue? You'd need to decrease the saturation, but RGB doesn't have a saturation axis. You'd have to move all three channels toward their average, which requires doing math that doesn't map to how designers think about color.

/* A vivid blue */
color: rgb(0, 100, 255);

/* A muted version? Now you need to think about channel ratios */
color: rgb(76, 128, 204);
Enter fullscreen mode Exit fullscreen mode

This is why alternative color models exist.

HSL maps to how humans describe color

HSL (Hue, Saturation, Lightness) rearranges the same RGB gamut into a cylinder:

  • Hue: the angle on the color wheel (0-360 degrees). Red is 0, green is 120, blue is 240.
  • Saturation: how vivid the color is (0% is gray, 100% is fully saturated).
  • Lightness: how light or dark it is (0% is black, 100% is white, 50% is the "pure" color).
/* Pure red in HSL */
color: hsl(0, 100%, 50%);

/* A muted, darker version */
color: hsl(0, 40%, 35%);
Enter fullscreen mode Exit fullscreen mode

This is dramatically more intuitive. Want a lighter version? Increase lightness. Want it more muted? Decrease saturation. The parameters map to the words we use to describe color adjustments.

But HSL has its own problem: perceptual non-uniformity. An HSL lightness of 50% doesn't look equally bright across all hues. Yellow at hsl(60, 100%, 50%) looks much brighter than blue at hsl(240, 100%, 50%), even though they have identical saturation and lightness values. This makes it unreliable for generating accessible color palettes.

Hex is just RGB in a trench coat

#3A86FF is not a different color model. It's RGB written in hexadecimal. The first two characters are the red channel (3A = 58), the next two are green (86 = 134), and the last two are blue (FF = 255). That's it.

The shorthand #F00 expands to #FF0000. The 8-digit form #3A86FF80 adds an alpha channel (80 = 128, roughly 50% opacity).

Hex is compact for writing but terrible for understanding. Looking at #3A86FF, can you tell it's a medium-brightness blue? Most people can't. That's why HSL exists.

The new CSS color functions

CSS has moved well beyond hex and basic rgb(). Modern browsers support:

/* OKLCH - perceptually uniform */
color: oklch(70% 0.15 240);

/* Relative color syntax - derive colors from a base */
color: oklch(from var(--brand) calc(l - 0.1) c h);

/* Color-mix - blend two colors */
color: color-mix(in oklch, #3A86FF 70%, white);
Enter fullscreen mode Exit fullscreen mode

OKLCH is the most significant development in CSS color in years. It's a perceptually uniform color space, meaning that equal numerical changes produce equal perceived visual changes. A lightness change of 0.1 looks like the same amount of change whether you're starting from a yellow or a blue.

The three OKLCH parameters are:

  • L: lightness, from 0% (black) to 100% (white)
  • C: chroma, essentially saturation (0 is gray, higher values are more vivid)
  • H: hue angle, similar to HSL but on a perceptually uniform wheel

Five mistakes developers make with color

  1. Using pure black text on pure white backgrounds. The contrast ratio of #000000 on #FFFFFF is 21:1, which sounds ideal but creates excessive eye strain for long reading. A dark gray like #1A1A2E or #2D3436 on white is easier to read for extended periods.

  2. Judging contrast by eye. WCAG contrast ratios exist because human perception of contrast is context-dependent. A color combination that looks fine on your high-brightness monitor in a well-lit office might be illegible on a phone screen in sunlight. Always check the math: 4.5:1 minimum for normal text, 3:1 for large text.

  3. Building palettes in RGB. If you're generating a series of shades for a design system, working in RGB means you're adjusting three interdependent values to achieve one perceptual change. Work in HSL at minimum, or OKLCH for best results.

  4. Forgetting color blindness. Roughly 8% of men and 0.5% of women have some form of color vision deficiency. Red-green color blindness is most common. Never use color alone to convey information -- always pair it with text labels, patterns, or icons.

  5. Ignoring dark mode. A color that works beautifully on a light background might become invisible or garish on a dark background. Design your palette with both modes in mind. HSL makes this easier: you can keep the hue and saturation constant while adjusting lightness for each mode.

Converting between formats

The conversion math is well-known:

RGB to Hex: convert each channel to base-16 and concatenate.

RGB to HSL: find the min and max channel values, compute hue from the channel ratios, saturation from the range, and lightness from the average of min and max.

In JavaScript:

// Quick hex to RGB
const hexToRgb = hex => {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
};
Enter fullscreen mode Exit fullscreen mode

For quick color picking and conversion between hex, RGB, HSL, and other formats, I built a color picker at zovo.one/free-tools/color-picker that shows all formats simultaneously so you can grab whichever representation you need.

Color on the web is a solved problem mathematically, but it's still an evolving problem perceptually. The gap between what the numbers say and what your eyes see is where the bugs live. Understanding color spaces -- even at a surface level -- makes you a better front-end developer and a more effective communicator with designers.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)