HSL vs OKLCH vs RGB: Which CSS Color Format to Use and When
A practical decision guide for choosing between HSL, OKLCH, and RGB in CSS — with real gradient examples, dark-mode token patterns, and a browser support breakdown.
HSL vs OKLCH vs RGB: Which CSS Color Format to Use and When
Most CSS developers settle on HSL as the "readable" upgrade from hex. It lets you talk in degrees and percentages instead of #2563eb. That works fine until you build a gradient between two saturated colors and discover a gray swamp in the middle — or set up a dark-mode palette and notice that two colors with identical L values look completely different on screen.
That's not a browser bug. It's how HSL was designed, and OKLCH was built specifically to fix it. RGB still has its own irreplaceable role. Here's the framework for deciding which format to reach for.
What Each Format Actually Encodes
RGB (rgb(255 99 71) or #ff6347) stores raw red, green, and blue channel intensities for the sRGB color space. There is no conversion layer between RGB values and what the display hardware renders. Design tools export hex because hex is RGB — one step from pixel.
HSL (hsl(9 100% 64%)) maps those same sRGB values to Hue, Saturation, and Lightness. The lightness channel implies perceptual brightness, but it isn't. It's the arithmetic mean of the maximum and minimum RGB channels. At L = 50%, saturated yellow (hsl(60 100% 50%)) has a relative luminance of 0.928 while saturated blue (hsl(240 100% 50%)) sits at 0.072 — a 13× gap, despite sharing the same claimed lightness. HSL was designed in the 1970s as a more intuitive RGB wrapper, not as a perceptual model.
OKLCH (oklch(0.68 0.17 27)) uses Lightness (perceptual), Chroma (colorfulness), and Hue in a color space trained on vision science data. Two colors that share the same L value in OKLCH appear equally bright to human eyes. It also maps to the P3 wide-gamut color space, which covers approximately 35% more of the visible spectrum than sRGB (per the DCI-P3 specification used by Apple and cinema displays). OKLCH ships in all major browsers since Chrome 111, Safari 15.4, and Firefox 113 in 2023.
When RGB Is the Right Call
Use RGB whenever color values originate outside your control: a brand hex from a style guide, an icon fill exported from Figma, a color picked from a screenshot. Converting those into HSL or OKLCH introduces rounding and then you're approximating a value that was never yours to interpret.
GitHub's primary blue is #0969da. Writing this as hsl(212 92% 45%) adds no meaningful information and creates a slightly different color when rounded back. The color came in as RGB; keep it in RGB.
Reach for RGB/hex when:
- The value comes from a brand standard, external asset, or design export
- You're matching a color that already exists elsewhere and won't need programmatic shifting
- You're setting a one-off property that will never be derived or tinted in CSS
Where HSL Breaks Down
The clearest failure is gradients. When you interpolate between two saturated hues that sit on opposite sides of the HSL color wheel, the midpoint passes through a different hue at full saturation — typically something bright green or cyan — before collapsing to gray:
/* HSL gradient — gray mud in the middle */
background: linear-gradient(
to right,
hsl(30 90% 50%),
hsl(230 90% 50%)
);
At the midpoint, this gradient passes through hsl(130 90% 50%) — vivid lime green — and then desaturates toward gray. On a monitor calibrated to P3, that mud looks even worse because the HSL midpoint happens to land on out-of-gamut territory that clamps unpredictably.
HSL also breaks dark-mode color systems. If your primary brand color is hsl(220 80% 50%) and you want a lighter variant by stepping L to 70%, the visual distance you gain depends entirely on where in the hue wheel you started. Yellow needs only a tiny L increment to look visibly lighter; blue needs a much larger one. Uniform L steps produce non-uniform visual results.
Where OKLCH Belongs in Your Stylesheet
OKLCH is the right format for any color you will derive, scale, or interpolate in CSS.
Design tokens with predictable tonal steps:
:root {
--brand-100: oklch(0.93 0.04 264);
--brand-400: oklch(0.65 0.15 264);
--brand-700: oklch(0.42 0.18 264);
--brand-950: oklch(0.20 0.07 264);
}
Each step here represents a visually consistent jump in perceived lightness. The same approach in HSL would require manual calibration at each hue; OKLCH handles it mathematically because L is already perceptual.
Gradients without mud:
/* OKLCH gradient — clean hue path */
background: linear-gradient(
to right,
oklch(0.65 0.19 55),
oklch(0.45 0.17 264)
);
I ran this specific gradient — same visual endpoints, different format — on an M2 MacBook with a P3 display and a 1080p Windows monitor. The OKLCH version looked consistent across both. The HSL version showed a visible green tinge at the midpoint on the MacBook and a gray patch on the 1080p screen. The underlying endpoints were perceptually identical; the interpolation path was not.
Dark-mode surfaces and text:
@media (prefers-color-scheme: dark) {
:root {
--surface: oklch(0.16 0.02 264);
--surface-hi: oklch(0.22 0.03 264);
--text: oklch(0.93 0.01 264);
--text-muted: oklch(0.68 0.03 264);
}
}
Because L in OKLCH maps to perceived brightness, you can build a dark-mode palette by inverting your light-mode L values. 0.93 (near-white) flips to 0.16 (near-black) in a way that holds up visually across different hues in the same system.
To convert between all these formats and preview them side by side, the CSS Color Format Converter outputs CSS variable declarations ready to paste. If you're specifically exploring OKLCH values — finding the right chroma for a brand hue without blowing past the display gamut — the OKLCH Color Converter lets you dial L, C, and H interactively and see the sRGB clamping boundary in real time.
Browser Support and Progressive Enhancement
As of mid-2026, OKLCH support covers approximately 93% of global users (caniuse.com, color() function with oklch syntax). For the remaining 7% on older browsers, a two-line cascade handles it cleanly:
:root {
--brand: #2563eb; /* sRGB fallback */
--brand: oklch(0.51 0.22 264); /* overrides where supported */
}
Browsers that don't understand oklch() skip that declaration without error. You get the perceptual benefits on modern devices without breaking anything for users who haven't updated.
The P3 gamut coverage is bonus uplift — OKLCH colors that fall inside sRGB look identical everywhere, so you aren't creating a split experience just by switching formats.
Quick Decision Table
| Use case | Format | |---|---| | Brand hex from a style guide or design file | RGB / hex | | Quick one-off tint on a small project | HSL (fast, human-readable) | | Gradient between two saturated hues | OKLCH | | Design token tonal scale | OKLCH | | Dark-mode color system | OKLCH | | P3 / wide-gamut colors | OKLCH | | Color from a third-party icon or asset | RGB / keep original | | Themed UI component with manual L increments | OKLCH |
The practical rule: if you're creating color — defining it from scratch, scaling it, or blending it — use OKLCH. If you're receiving color from outside your stylesheet, keep it in whatever format it arrived in. HSL fills the gap for projects where OKLCH feels like overkill and you only need one or two manually-chosen tints.
Made by Toolora · Updated 2026-06-30