Skip to content

Guide

CSS color formats: hex, RGB, HSL, OKLCH — which to use when

Four formats, four use cases. The right pick depends on what you're trying to do with the colour.

By Published Updated

Modern CSS supports four primary colour notations: hex (#FF6600), rgb(),hsl(), and the newer oklch(). All describe the same colour gamut (sRGB by default, wider for OKLCH). They differ in human readability, ease of manipulation, and what kind of colour change feels natural.

The four formats at a glance

FormatExampleBest for
Hex#FF6600Storing colours, exchanging with designers, code that doesn’t modify the value.
rgb()rgb(255 102 0)Programmatic generation, when the channel-by-channel values matter.
hsl()hsl(24 100% 50%)Manually tweaking lightness and saturation while keeping the hue locked.
oklch()oklch(0.72 0.18 47)Designing colour scales where perceptual uniformity matters.

Hex: the storage format

Hex encodes each RGB channel as a two-digit hexadecimal value. #FF6600means red 255, green 102, blue 0 — that’s a vivid orange.

Pros: short, universally understood, copy-paste friendly, works in every CSS context.

Cons: opaque to read (“is #7E4F2Ba warm or cool tone?”), can’t easily adjust lightness or saturation without converting first.

Modern CSS supports 8-digit hex for transparency:#FF6600AA is the orange above at 67% opacity (AA = 170 / 255).

rgb(): the explicit channel format

Same channels as hex, decimal numbers, optional alpha. Two syntaxes coexist:

  • Legacy comma form: rgb(255, 102, 0) or with alpha rgba(255, 102, 0, 0.67)
  • Modern space form: rgb(255 102 0) or with alpha rgb(255 102 0 / 0.67)

Best for programmatic generation: building colour from algorithm output, blending two colours, computing accessibility contrast.

hsl(): the human-tweakable format

Three values: hue (0-360°), saturation(0-100%), lightness (0-100%). Adjusting each independently feels more natural than tweaking RGB channels.

  • Hue: the colour itself. 0 = red, 60 = yellow, 120 = green, 180 = cyan, 240 = blue, 300 = magenta.
  • Saturation: 0% = grey, 100% = pure colour.
  • Lightness: 0% = black, 50% = pure hue, 100% = white.

Best for: design tokens where you want a “same colour, darker” variant — drop the lightness by 10%, keep hue and saturation. Hover states, theme variants, and gradients all benefit.

The catch: HSL’s lightness doesn’t track perceived brightness uniformly. HSL yellow at 50% lightness looks much lighter than HSL blue at 50% lightness. For perceptually-uniform colour design, OKLCH is the better choice.

oklch(): the perceptual format

Three values: L (lightness, 0-1),C (chroma, 0-~0.4 for sRGB-displayable),H (hue, 0-360°). Based on the OKLab perceptual colour space (Björn Ottosson, 2020).

The key property: equal lightness values look equally bright. oklch(0.6 0.2 0) (red) andoklch(0.6 0.2 120) (green) andoklch(0.6 0.2 240) (blue) are all the same perceived brightness, unlike HSL where these would vary dramatically.

Best for: designing colour scales, dark-mode themes, ensuring text contrast holds across a hue rotation. Supported in all evergreen browsers since 2023.

The catch: chroma maxes out at different values per hue. High-chroma blue is achievable; high-chroma yellow runs into sRGB gamut limits earlier. Use a chroma value the browser can render at every hue you need, or accept clipping at the extremes.

Picking the right one

  • For brand colours and design tokens:define in HSL or OKLCH; that’s what designers can tweak meaningfully. Store hex as a fallback in legacy files.
  • For UI colour ramps (50/100/200/.../900):OKLCH with a fixed chroma and varying lightness produces perceptually-even scales. HSL works but the visual spacing is uneven.
  • For copying from a Figma/Photoshop spec:paste hex. Convert via our hex to RGB tool or a CSS preprocessor when you need to manipulate.
  • For accessibility-driven choices: compute WCAG contrast on the rgb form. The contrast formula operates on linear RGB, not on hex or HSL directly.

The CSS custom-property pattern

Modern theme architectures store colours as HSL/OKLCH custom properties so they can be derived:

:root {
  --primary-hue: 24;
  --primary: oklch(0.72 0.18 var(--primary-hue));
  --primary-hover: oklch(0.65 0.18 var(--primary-hue));
  --primary-bg: oklch(0.92 0.05 var(--primary-hue));
}

Changing the hue once rotates the entire palette consistently. The same pattern with hex is impossible without preprocessor functions.

The takeaway

Hex for storage and copy-paste. RGB for programmatic. HSL for designer tweaking. OKLCH for perceptually-uniform ramps. The CSS spec lets you mix freely; pick the format that matches what you’re doing with the colour.

Sources: CSS Color Module Level 4 (W3C); Björn Ottosson,A perceptual colour space(2020); MDN’s Modern CSS Colour Functions documentation.

Worked example: building a 10-step UI colour ramp

A design-system ramp typically runs 50 / 100 / 200 / 300 / 400 / 500 / 600 / 700 / 800 / 900 — lightest to darkest — with 500 as the “true” brand colour. The question is what each intermediate step should look like.

Using HSL with constant hue and saturation, varying lightness from 95% (step 50) to 15% (step 900):

--brand-50:  hsl(24 100% 95%);
--brand-100: hsl(24 100% 90%);
--brand-200: hsl(24 100% 80%);
--brand-300: hsl(24 100% 70%);
--brand-400: hsl(24 100% 60%);
--brand-500: hsl(24 100% 50%);  /* base */
--brand-600: hsl(24 100% 42%);
--brand-700: hsl(24 100% 34%);
--brand-800: hsl(24 100% 26%);
--brand-900: hsl(24 100% 18%);

Problem: HSL lightness is mathematically uniform but perceptually isn’t. The jump from brand-400 to brand-500 looks dramatic; the jump from brand-800 to brand-900 is barely visible. The ramp feels uneven.

OKLCH with constant chroma, varying lightness from 0.97 to 0.20:

--brand-50:  oklch(0.97 0.04 47);
--brand-100: oklch(0.93 0.07 47);
--brand-200: oklch(0.86 0.11 47);
--brand-300: oklch(0.78 0.15 47);
--brand-400: oklch(0.71 0.18 47);
--brand-500: oklch(0.64 0.21 47);  /* base */
--brand-600: oklch(0.55 0.20 47);
--brand-700: oklch(0.46 0.17 47);
--brand-800: oklch(0.36 0.13 47);
--brand-900: oklch(0.26 0.09 47);

Each step looks like one perceptually-uniform step from the previous. Chroma tapers at the dark end because high-chroma colours simply don’t exist at low lightness in sRGB — physics, not a tooling choice. Tailwind v4, Radix Colors v3, and most modern design systems switched to OKLCH ramps after 2023 for exactly this reason.

Common mistakes when picking a colour format

  • Specifying transparency two different ways inconsistently. rgba(255 102 0 / 0.67) and #FF6600AB are the same colour, but having both in one stylesheet makes search-and-replace and theming brittle. Pick one notation per project.
  • Trusting OKLCH at extreme chroma. oklch(0.6 0.4 47)requests a chroma the sRGB display gamut can’t actually render; the browser clips to the nearest renderable value. Verify your design intent renders correctly on real displays, or constrain chroma to the safe sRGB range (max ~0.25 for warm hues, ~0.15 for green-yellow).
  • Computing WCAG contrast on HSL or OKLCH values. The WCAG 2.1 contrast formula operates on linear-light sRGB. Tools that take HSL or OKLCH inputs internally convert to sRGB first; computing the formula manually requires the conversion. APCA (the WCAG 3 draft contrast metric) handles perceptual uniformity natively and is worth investigating for new projects.
  • Hard-coding hex in components. color: #FF6600 inside a component locks the colour to one value; theme-switching becomes a search-and-replace exercise. Always reference custom properties (var(--brand-500)) defined at the root, not literal hex values.
  • Using color: red or other named colours. CSS named colours (red =#FF0000, orange =#FFA500) are technically supported but have garish, undesigned sRGB primaries that rarely match a brand palette. Treat them as debug colours, not production colours.

When the CSS colour-format choice does NOT matter

  • Static one-off pages with no theming. A landing page with 5 colours and no dark mode: pick hex, move on. The OKLCH advantages only pay off when you’re generating ramps or supporting multiple themes.
  • Print-bound output.Anything destined for CMYK printing needs to be converted from sRGB through an ICC profile to the target press’s colour space. The CSS notation is irrelevant; what matters is the source profile and rendering intent. See our RGB vs CMYK comparison for the gamut and conversion details.
  • Email HTML.Outlook 2007-2019 and some webmail clients still don’t support modern CSS colour functions reliably. Email templates should fall back to 6-digit hex; OKLCH and 8-digit hex break in Outlook desktop.
  • Strict P3 / Display P3 wide-gamut design. The Apple Display P3 colour space accessible via color(display-p3 ...) is a different discussion entirely — see the P3 glossary entry and our gamut glossary entry for when wide-gamut output is worth the complexity.

For the full spec details, the CSS Color Module Level 4 covers every notation, and Björn Ottosson’s original OKLab post (2020) is the canonical reference for the perceptual model underneath OKLCH. For interactive format conversion, the colour converter handles hex ↔ rgb ↔ hsl ↔ oklch with WCAG contrast reporting.

Performance considerations

The four notations are computationally equivalent at render time — the browser converts everything to linear RGB internally before painting. Performance differences between notations are within measurement noise on any modern device. What can cost performance is:

  • Heavy use of color-mix() with custom-property arguments. Eachcolor-mix() call resolves at paint time; using hundreds in a single render frame can show up in Lighthouse audits. Precompute mixes at build time for static palettes.
  • Wide-gamut color(display-p3 ...) on devices without P3 displays. The browser performs gamut-mapping per element. Free on M-series Macs and recent iPhones; measurable cost on older Windows hardware.
  • Layered semi-transparent backgrounds. Alpha-channel composition is fast on modern GPUs but slow on low-end Android. Stacking 5+ transparent layers can trigger paint storms on the bottom 20% of devices.

Browser support and fallback strategy

Browser support as of early 2026:

NotationChromeSafariFirefoxOutlook desktop
Hex 6-digitallallallall
Hex 8-digit (alpha)62+ (2018)10+ (2016)49+ (2016)NO
rgb() legacy commaallallallall
rgb() space syntax65+ (2018)15+ (2021)113+ (2023)NO
hsl()all modernall modernall modernpartial
oklch()111+ (2023)15.4+ (2022)113+ (2023)NO
color-mix()111+ (2023)16.2+ (2022)113+ (2023)NO

For email and legacy-browser contexts, the safe target is 6-digit hex plus separate alpha-channel solutions (semi-transparent PNG fallbacks). For modern web, OKLCH is fully usable; the rare downlevel browser is likely also failing on more important features and deserves a separate strategy rather than colour compromises.

The CSS @supports at-rule lets you progressive-enhance: define a hex fallback first, then an OKLCH override inside @supports (color: oklch(0 0 0)) for browsers that handle the new syntax. In practice few sites bother — the OKLCH-supporting cohort is now over 96% of global browser traffic per Can I Use data, and the visual difference between an OKLCH ramp and an HSL fallback is small enough that the simplest strategy is to ship one set of values and accept the 3-4% who see an HSL approximation.

Frequently asked questions

What is the difference between hex and HSL colour formats in CSS?
Hex encodes colour as three 2-digit hexadecimal values for red, green, and blue (e.g. #FF6600). HSL expresses the same colour as hue (0–360°), saturation (0–100%), and lightness (0–100%), making it easier to adjust brightness without recalculating all three channels.
When should I use oklch instead of hsl in CSS?
Use oklch when you need perceptually uniform colour gradients or palette generation. In HSL, changing hue while holding lightness constant produces colours that look very different in brightness; oklch's lightness channel is calibrated to human perception, so L=60% looks equally bright across all hues.
Can I use 8-digit hex codes in CSS for transparency?
Yes. CSS supports 8-digit hex (#RRGGBBAA) where the last two digits specify alpha from 00 (transparent) to FF (opaque). For example, #FF660080 is a 50% transparent orange.
Do all browsers support oklch colour values?
oklch has been supported in all major browsers since 2023 (Chrome 111, Firefox 113, Safari 15.4). For older browser support, provide a hex or rgb fallback before the oklch declaration.
What is the advantage of hsl over rgb for generating colour palettes?
HSL lets you create tints and shades by adjusting only the lightness value while keeping hue and saturation constant, producing cohesive palettes. With RGB, creating a lighter version of #FF6600 requires manually recalculating all three channels.
How do I convert a hex colour to hsl?
Normalise each RGB channel to 0–1, find the max and min, then compute: H = colour-angle based on which channel is max, S = (max−min)/(1−|2L−1|), L = (max+min)/2. Most design tools and CSS preprocessors include built-in conversion functions.

Sources & references

Authoritative references cited by this piece. Verified by Buğra Sözeri on the dates shown and re-checked at every deploy.

Related

Published May 16, 2026 · Last reviewed May 31, 2026