CSS Color Formats Compared: Hex, rgb(), hsl(), hwb(), oklch(), color-mix() and When to Use Each
CSS now has more than ten ways to write a colour, and they are not interchangeable conveniences. Hex and `rgb()` describe the same sRGB colours in different notation. `hsl()` and `hwb()` rearrange those colours for easier adjustment. `lab()`, `lch()`, `oklab()` and `oklch()` describe colours by how people perceive them and can reach colours beyond sRGB on modern displays. Knowing which does what makes design tokens, themes and hover states much simpler.
The formats at a glance
.a { color: #3366cc; } /* hex: 3, 4, 6 or 8 digits */
.b { color: rgb(51 102 204); } /* red, green, blue 0-255 */
.c { color: hsl(220 60% 50%); } /* hue, saturation, lightness */
.d { color: hwb(220 20% 20%); } /* hue, whiteness, blackness */
.e { color: oklch(53% 0.17 262); } /* lightness, chroma, hue (perceptual) */
.f { color: lab(44% 11 -59); } /* CIE Lab */
.g { color: color(display-p3 0.2 0.4 0.8); } /* explicit colour space */
.h { color: royalblue; } /* one of ~150 named colours */
.i { border-color: currentColor; } /* the element's own color value */
Lines .a to .f are the same blue, with the OKLCH and Lab values rounded; .g is a slightly different, wider-gamut version of it. All of these are supported in current versions of Chrome, Edge, Firefox and Safari. The space-separated syntax (rgb(51 102 204)) is the modern form from CSS Color Level 4; the comma form (rgb(51, 102, 204)) still works everywhere.
Transparency
Every functional notation takes an optional alpha after a slash, and hex takes a fourth or eighth digit. rgba() and hsla() are now plain aliases of rgb() and hsl().
#3366cc80 /* 80 hex = 128/255 ≈ 50% opacity */
#36c8 /* short form: 8 -> 88 ≈ 53% */
rgb(51 102 204 / 0.5)
hsl(220 60% 50% / 50%)
oklch(53% 0.17 262 / 0.5)
transparent /* = rgb(0 0 0 / 0) */
Eight-digit hex is easy to misread. The alpha digits are hexadecimal, so 80 is 50% and CC is 80%. Gradients to transparent used to fade through grey in some browsers because transparent is transparent black. Modern browsers interpolate in premultiplied alpha, which fixes it, but rgb(51 102 204 / 0) states the intent explicitly.
Hex and rgb(): the same colours, different notation
Hex is RGB in base 16: #3366cc is red 33 (51), green 66 (102), blue cc (204). Three-digit hex doubles each digit, so #36c is #3366cc. Hex is compact, ubiquitous in design tools and easy to copy, but unhelpful for adjustments: nobody can tell by looking that #2952a3 is a darker #3366cc.
Both describe colours inside sRGB, the colour space the web has used since the 1990s. Modern phones and laptops can display more saturated colours, the Display P3 gamut, that hex and rgb() cannot express.
hsl() and hwb(): built for adjusting
hsl() separates the hue angle from saturation and lightness, so shades of one brand colour differ in one number: hsl(220 60% 50%), hsl(220 60% 40%), hsl(220 60% 30%). hwb() describes the same colours as a hue plus how much white and black to mix in, which some people find closer to mixing paint.
The catch is that HSL lightness is arithmetic, not perception. Two colours that both sit at 50% can behave completely differently as text: hsl(60 100% 50%) yellow on white has a contrast ratio of about 1.07:1, while hsl(240 100% 50%) blue on white reaches about 8.6:1. The perceptual formats in the next section exist to close that gap.
oklch() and lab(): perceptual colour and wider gamuts
oklch() uses the Oklab colour space, designed so that equal numeric steps look like equal visual steps. L is perceived lightness (0–100%), C is chroma, or colourfulness, and H is the hue angle. Two colours with the same L look about equally light, whatever their hue, which makes consistent palettes and predictable contrast much easier.
:root {
--blue-500: oklch(55% 0.17 262);
--green-500: oklch(55% 0.17 150); /* same L: similar perceived lightness */
--blue-600: oklch(48% 0.17 262); /* darker: lower L, same hue */
}
OKLCH can also describe colours outside sRGB. On a Display P3 screen, oklch(70% 0.25 150) shows a greener green than any hex code can produce. On an sRGB screen the browser maps it to the nearest displayable colour. If you need exact output on older displays, provide an sRGB fallback first, since browsers ignore declarations they do not understand:
.accent {
color: #1fa854; /* sRGB fallback */
color: oklch(70% 0.25 150); /* wider gamut where supported */
}
lab() and lch() are the older CIE equivalents. They are perceptual too, but have known hue shifts in blues, which Oklab was designed to fix. For new work, OKLCH is usually the better choice.
color-mix() and relative colours
Two newer features remove most of the need to hard-code shades. color-mix() blends two colours in a chosen colour space. Relative colour syntax takes an existing colour, including a CSS variable, and changes individual channels.
.button:hover { background: color-mix(in oklch, var(--brand) 85%, black); }
.tint { background: color-mix(in srgb, var(--brand) 15%, white); }
/* relative colour: same colour at 40% opacity, or 8% darker */
.overlay { background: rgb(from var(--brand) r g b / 40%); }
.pressed { background: oklch(from var(--brand) calc(l - 0.08) c h); }
The mixing space matters: mixing blue and yellow in srgb passes through grey, while in oklch keeps the result vivid. Both features are supported in current versions of all major browsers, relative colour syntax since 2024. Provide a static fallback if you still support older browsers.
Which format to use
For copying colours from design tools, hex is fine. For design tokens and themes, oklch() gives the most predictable palettes and is easiest to adjust. For small tweaks such as hover states and tints, use color-mix() or relative colours based on a token instead of adding new hex values. currentColor keeps icons and borders in sync with text colour, and named colours are fine for prototypes but rarely match a brand.
Whatever you choose, keep one source of truth per colour in a custom property, and derive the variants from it.
Sources and further reading
CSS Color Module Level 4 defines hex with alpha, the space-separated rgb() and hsl() syntax, hwb(), lab(), lch(), oklab(), oklch() and color(). CSS Color Module Level 5 defines color-mix() and relative colour syntax. MDN's compatibility tables list browser support, and Björn Ottosson's Oklab article explains the perceptual model.
Frequently asked questions
What is the best colour format in CSS?
For design tokens and palettes, `oklch()`, because its lightness is perceptual and it reaches wide-gamut colours. Hex is fine for colours copied from design tools. Use `color-mix()` or relative colours to derive hover and tint variants.
What is the difference between hex and rgb() in CSS?
None in the colours they can express; they are two notations for sRGB. `#3366cc` equals `rgb(51 102 204)`. Hex is shorter, while `rgb()` makes the channel values readable.
How do I add transparency to a hex colour?
Add two hex digits for alpha: `#3366cc80` is about 50% opacity. Or use functional notation: `rgb(51 102 204 / 0.5)`.
Is rgba() deprecated?
Not removed, but no longer needed. In CSS Color Level 4, `rgba()` and `hsla()` are aliases of `rgb()` and `hsl()`, which accept alpha after a slash.
What is oklch in CSS?
A colour function using the Oklab perceptual colour space: lightness, chroma and hue. Equal lightness values look equally light across hues, and it can express colours beyond sRGB on wide-gamut displays.
How do I make a colour darker in CSS without a new hex value?
Use `color-mix(in oklch, var(--brand) 85%, black)`, or relative colour syntax such as `oklch(from var(--brand) calc(l - 0.08) c h)`.