tooldura

Developer Tools

Why HSL Says Yellow and Blue Are Equally Light

T
tooldura editorial
7 min readUpdated August 22, 2026Open tool →

Pure yellow is hsl(60, 100%, 50%). Pure blue is hsl(240, 100%, 50%). By the numbers they are the same lightness. Put white text on each and one is unreadable while the other is fine, because the yellow leaves your screen carrying about thirteen times the light of the blue. The 50% was never measuring that.

The Byte Is Not the Light

Everything starts with what a channel value means. In sRGB, the space almost every screen and image file assumes, the byte you write is not proportional to the light the display emits. It is encoded through a transfer function defined in IEC 61966-2-1: divide by 12.92 below 0.04045, and otherwise raise ((c + 0.055) / 1.055) to the power 2.4.

Run #808080 through it. The channel is 128 of 255, half the range, and it comes out at 0.216 of the luminance of white. Grey at the exact middle of the byte range emits roughly a fifth of the light of the page around it.

The curve is not a mistake, it is the point. Human vision is far more sensitive to differences among dark tones than among bright ones, so spending half the available codes on the bottom fifth of the light is exactly how you avoid banding in shadows with only 8 bits per channel. #808080 looks like a middle grey because it is one perceptually: its CIE lightness, L*, is 53.6 out of 100. The value that actually emits half the light of white is #bcbcbc, and it looks distinctly pale.

Where HSL Goes Wrong

HSL was designed in the 1970s to give artists a handle on RGB that matched how they talked about color, and as a control surface it works: hue turns, saturation drains, lightness climbs. What it does not do is measure anything.

HSL lightness is the midpoint between the largest and smallest of the three sRGB channels, computed on the encoded bytes. It never looks at how much of that light is green, and green is where nearly three quarters of perceived brightness comes from: the coefficients in the luminance formula are 0.2126 red, 0.7152 green, 0.0722 blue.

So yellow, which is red plus green at full strength, lands at luminance 0.928, and blue on its own lands at 0.072. Both are 50% lightness in HSL. The contrast ratio between them is 8.0:1, which is more than most sites manage between their body text and their background.

This is why a palette built by walking HSL lightness in even steps comes out uneven, and why the same 40% lightness gives you a usable dark blue and a muddy dark yellow.

Which Format for Which Job

Six notations for the same colors, and the one thing each is actually good at.

FormatExampleReach for it when
HEX#4ade80Storing and pasting. Compact, universal, and the closest thing the web has to a canonical form
RGBrgb(74 222 128)Working with a channel numerically, or reading pixels out of a canvas
HSLhsl(142 69% 58%)Nudging a hue by hand, or dropping saturation. Not for comparing lightness
HSV / HSBhsv(142 67% 87%)Matching what Photoshop and Figma show in their pickers
CMYKcmyk(67% 0% 42% 13%)Handing a starting point to a printer. Never in CSS, which has no cmyk()
OKLCHoklch(80% 0.18 152)Building scales, mixing, and any change where the lightness has to behave
🎨

The named colors are an archaeological site

CSS took its first sixteen names from the Windows VGA palette, which is why green is #008000. The X11 list that CSS Color 3 later absorbed already had a green, and it was the bright #00ff00 we now call lime. The two lists disagreed about gray as well: X11's was #bebebe, CSS kept VGA's #808080. Both gray and grey work, because the specification gave up and defined all of them. The newest name, rebeccapurple (#663399), was added to CSS Color 4 in 2014 in memory of Eric Meyer's daughter Rebecca, who died at six.

What OKLCH Changes

OKLab is a color space published by Björn Ottosson in December 2020, built by fitting a simple model to the same perceptual datasets that older spaces such as CIELAB were fitted to, while avoiding CIELAB's known hue distortions in blue and purple. OKLCH is the same space read in polar form: lightness, chroma, hue.

The practical difference is that its lightness axis is honest. Yellow comes out at 96.8% and blue at 45.2%, which is what your eyes have been saying all along. Two colors with the same L in OKLCH read as equally light whatever their hue, so a scale generated by stepping L is even by construction.

CSS Color Module Level 4 standardised oklab() and oklch(), and browsers shipped them between 2022 and 2023. Tailwind CSS v4 moved its whole default palette into OKLCH in 2025, which is the clearest signal that this is no longer an experiment.

The catch is gamut. OKLCH can name colors sRGB cannot show, so a high chroma at an extreme lightness has to be mapped back in. The right way is to hold the lightness and hue and lower the chroma until the color fits, which is what CSS Color 4 specifies and what this site's picker does when it builds a tint scale. Clipping the RGB channels instead is faster and shifts the hue, which is how a ramp ends up turning slightly pink on its way to black.

Read one color in every notation

HEX, RGB, HSL, HSV, CMYK and OKLCH at once, with contrast scores and a tint scale.

Open Color Picker →

Rules That Follow From This

None of this is theory you have to hold in your head. It collapses into a handful of habits.

1

Generate scales in OKLCH, ship them as hex

Step the lightness, keep hue and chroma, and let the tool clamp what falls out of gamut. Then paste the hex values, which every browser and design tool understands without argument.

2

Never compare two colors by their HSL lightness

If the question is which of these is lighter, or will white text hold up, the answer comes from luminance or OKLCH lightness. HSL will confidently tell you they are the same.

3

Fade with opacity, not with a lighter hex

A translucent color composites against whatever is behind it, so it survives a theme switch. A hex hand-picked to look faded on white becomes invisible on dark.

4

Check contrast against the background you will really use

A ratio against white means nothing if the component sits on your surface-2 grey. Composite first, then measure, and remember an alpha below 1 changes the answer.

5

Treat CMYK output as a conversation starter

Without an ICC profile there is no single right answer, and bright cyan and pure green have no CMYK equivalent at all. Send the hex and let the printer's profile do the conversion.

Contrast Is Computed on Light, Not on Bytes

The WCAG 2.1 contrast ratio is (L1 + 0.05) / (L2 + 0.05), where each L is relative luminance: the channels linearised through the sRGB transfer function, then weighted 0.2126, 0.7152 and 0.0722. The 0.05 stands in for light reflecting off a real screen, which is why the ratio tops out at 21:1 rather than running to infinity.

Success criterion 1.4.3 asks for 4.5:1 on body text and 3:1 on text from 18.66px bold or 24px regular upwards. 1.4.6 raises those to 7:1 and 4.5:1 for AAA. 1.4.11 applies the 3:1 line to icons, form borders and other non-text things you are expected to be able to see.

The formula has a known weakness: it was derived for dark text on light backgrounds and it flatters light text on dark ones, where it tends to pass combinations people find hard to read. That is the reason APCA exists and is being considered for WCAG 3. Until something replaces it, 2.1 is what conformance is measured against, so it is what a checker should report.

Frequently Asked Questions

Related Tools

Keep Reading