A designer and a developer walk into a meeting. The designer wants to “darken the button by about 15%.” The developer is staring at #3366FF and trying to figure out what 15% darker even means, because subtracting 15% from each channel gives a different answer than subtracting 15% from the perceived brightness, and neither of them is what the designer had in mind.
This is a problem that hex notation cannot solve. Hex is a compact storage format, not a way to reason about colour. If you want to reason about colour, lightness, saturation, hue, you need a notation that exposes those dimensions directly.
There are three candidates in mainstream CSS. Here is when to use each, and why the third one is the one you should probably be using from now on.
Hex, the compact storage format
#3366FF means red 51, green 102, blue 255. It is a serialization of the three channels a screen actually uses, written in base 16 for brevity.
When hex wins
- Portability. Every design tool, stylesheet, documentation page, Slack message, and email understands hex. A CSV of hex codes round-trips through any pipeline.
- Brevity. Six characters. Shorter than the alternatives.
- Zero cognitive overhead at the receiving end. A hex code is unambiguous. There is no colour space to clarify, no unit confusion. The browser reads three bytes and sends them to the GPU.
When hex loses
- Reasoning about relationships. Similar-looking colours often have radically different hex codes. Related hex codes often look nothing alike.
- Lightening and darkening. There is no linear operation on hex that produces a predictably-lighter or -darker version.
- Accessibility checks. Contrast ratios depend on perceived luminance, not on raw RGB. You cannot eyeball contrast from hex.
Hex is the right format for storing and transmitting colours. It is the wrong format for designing colours. Most teams get this backwards, they author in hex, then struggle to maintain the design system because every colour is a magic number with no relationship to any other.
HSL, the human-friendly compromise
HSL stands for Hue, Saturation, Lightness. hsl(230, 100%, 60%) is roughly the same colour as #3366FF. The components:
- Hue is an angle on a colour wheel, 0–360 degrees. 0 is red, 120 is green, 240 is blue, 360 loops back to red.
- Saturation is a percentage. 0% is completely grey, 100% is the most saturated version of that hue at the given lightness.
- Lightness is a percentage. 0% is black, 50% is the pure colour, 100% is white.
When HSL wins
- Darkening and lightening. Lower the lightness by 10%, you get a darker version of the same colour. That is the whole pitch.
- Consistent palettes. Keep the hue constant, vary lightness and saturation, you get a coherent shade/tint palette. Vary the hue by fixed increments, you get a colour wheel.
- Readability. A developer can read
hsl(230, 100%, 60%)and immediately know it is a vivid blue at medium-high lightness.#3366FFrequires conversion or memorization.
When HSL loses
- Perceptual inconsistency. HSL’s lightness is a mathematical average of the channels, not a match to how the human eye perceives brightness.
hsl(60, 100%, 50%)(pure yellow) looks vastly brighter thanhsl(240, 100%, 50%)(pure blue), even though both claim 50% lightness. For design-system work, this is a real problem. - Saturation means different things at different lightnesses. 100% saturation at 50% lightness is much more intense than 100% saturation at 90% lightness, which is almost white. You cannot reason about saturation in isolation.
HSL is a huge improvement over hex for design work. It is still mathematically naive about human perception. Which brings us to the third candidate.
OKLCH, the perceptually-uniform future
oklch(55% 0.22 263) is roughly the same colour as #3366FF. The components:
- L (Lightness) is a percentage, 0% (black) to 100% (white). Unlike HSL, this lightness is perceptually uniform, 50% is approximately halfway between black and white as the human eye sees it, not as the channels average it.
- C (Chroma) is roughly saturation, but unbounded. Typical values are 0 (grey) to about 0.37 (the most vivid colours your screen can render). Values above the gamut of your monitor get clipped.
- H (Hue) is an angle, same as HSL but with slightly different numbering because the underlying colour space is different.
OKLCH is based on the Oklab colour space, proposed by Björn Ottosson in 2020, specifically designed to match human perception of colour differences better than HSL, LAB, LCH, or any of the other acronyms you might have encountered. It is supported in all major browsers as of 2023.
When OKLCH wins
- Perceptually uniform lightness. Go from
oklch(60%)tooklch(50%)and the colour looks, to human eyes, uniformly 10% darker. This works across hues. Yellow at 60% and blue at 60% look equally bright. - Clean gradients. Interpolating between two OKLCH colours produces smooth, perceptually even gradients. Interpolating between two hex or HSL colours often produces a muddy middle zone.
- Accessibility-friendly. Contrast ratios calculated against perceptually uniform lightness values are more reliable predictors of actual legibility.
- Design-system-friendly. A shade palette generated by varying lightness from 10% to 95% at a fixed hue gives you a tidy, visually balanced scale. HSL does not, because HSL’s lightness is uneven.
When OKLCH loses
- Brand-new. Tool support is catching up. Figma added OKLCH support in 2024. Many older design tools still only speak hex, HSL, or proprietary Lab variants.
- Verbose syntax.
oklch(55% 0.22 263)is harder to scan than#3366FF. - Chroma is unbounded and gamut-sensitive. A chroma value that looks great on a P3 display might be clipped on an sRGB display. You need to test on target devices or write wrapper utilities.
- Documentation and shared knowledge are still thin. Developers new to OKLCH have less StackOverflow to lean on.
A concrete recommendation
If you are building anything larger than a one-page website in 2026, author your design tokens in OKLCH and serialize them to hex at build time.
/* authored */
--brand: oklch(55% 0.22 263);
--brand-hover: oklch(50% 0.22 263);
--brand-disabled: oklch(70% 0.08 263);
/* served (after build transform, or direct if you prefer) */
--brand: #3366FF;
The authored values let you reason about relationships, hover is 5% darker, disabled is lighter and less saturated, the hue is constant across states. The served values give you maximum browser compatibility and the fewest surprises in production.
If you cannot use OKLCH yet because your tool chain does not support it, use HSL instead. HSL is a genuine improvement over hex for reasoning, even if it is not perceptually uniform. Anything is better than making every designer-developer handoff a conversion puzzle.
And keep hex for what hex is good at: transmission. Writing a hex code in a commit message, a support ticket, a Slack thread, or an adoption URL on colour.love. That is where hex shines.
The thing to take away
Different notations for the same colour are not interchangeable even though they are mathematically equivalent. Each one makes certain operations easy and certain operations awkward. Choose the notation for the operation you are performing. Hex for storage, HSL for scripting, OKLCH for design. Convert at the boundary.
The old habit of hard-coding #FF5733 in every stylesheet is the source of every “but why doesn’t the button get darker when hovered” conversation in every product meeting you have ever sat through. Break the habit. Your design system will thank you. So will the designer who wanted 15% darker.