CSS Color Formats: HEX, RGB, HSL and OKLCH

HEX and rgb() are the same sRGB color written two ways, hsl() is easier for humans to tweak, and oklch() is the best choice for building palettes because its lightness matches what people actually see. All of them work in every current major browser.

CSS now gives you more ways to write a color than most designers will ever need. The good news is that they fall into two groups: formats that describe the familiar sRGB colors in different notations (hex, rgb, hsl, hwb), and newer formats built on perceptual color models (oklab and oklch). Knowing which group you are in tells you most of what you need.

HEX: the compact classic

A hex code is three pairs of hexadecimal digits for red, green and blue, each from 00 (0) to ff (255). #4f46e5 means red 79, green 70, blue 229. You can shorten a code when each pair is a repeated digit, so #ffcc00 becomes #fc0.

8-digit hex for transparency

Add a fourth pair and you get an alpha channel: #4f46e580 is the same indigo at about 50% opacity. The alpha pair runs from 00 (invisible) to ff (solid). A 4-digit shorthand also exists, such as #0008 for semi-transparent black. Common alpha values are worth memorizing: 40 ≈ 25%, 80 ≈ 50%, bf ≈ 75%.

Hex is what most design tools export, which is why it remains the default in many codebases. Its weakness is that you can't read it. Nobody can look at #4f46e5 and say how to make it 10% lighter.

rgb(): the same color, in decimal

rgb(79 70 229) is exactly #4f46e5, just written in base 10. Modern CSS uses space-separated values and an optional alpha after a slash: rgb(79 70 229 / 0.5). The older comma syntax, rgba(79, 70, 229, 0.5), still works everywhere, and rgba() is now simply an alias of rgb().

Converting between hex and rgb is pure arithmetic: each hex pair is a number from 0 to 255. The HEX to RGB converter does it instantly in both directions and also gives you the HSL value.

hsl(): hue, saturation, lightness

HSL rearranges sRGB into terms people find more intuitive. Hue is an angle on the color wheel (0 is red, 120 green, 240 blue), saturation is how vivid the color is, and lightness runs from black at 0% to white at 100%. Our indigo is roughly hsl(243 75% 59%).

That makes quick edits easy. Want a darker hover state? Lower the lightness. Want a muted version? Lower the saturation. HSL is still a good choice for one-off tweaks in a stylesheet.

The problem with HSL lightness

HSL lightness is a math shortcut on the RGB channels, not a measure of perceived brightness. Compare hsl(60 100% 50%) (yellow) with hsl(240 100% 50%) (blue). Both claim 50% lightness, yet the yellow looks dramatically brighter. If you build a palette by keeping HSL lightness constant and rotating the hue, the results will have uneven contrast, and white text that passes on one swatch may fail on the next.

hwb(): hue, whiteness, blackness

hwb() describes a color as a hue plus how much white and how much black is mixed in, for example hwb(243 27% 10%). It mirrors how painters think about tints and shades, and it is supported in all current major browsers. In practice it is rarely used, because it shares HSL's lack of perceptual accuracy and most teams moving beyond hex go straight to OKLCH.

oklab() and oklch(): perceptual color

OKLab is a color space designed so that equal numeric steps look like roughly equal visual steps. oklab() uses lightness plus two axes (green–red and blue–yellow). oklch() expresses the same space in a friendlier polar form:

  • L (lightness): 0% is black and 100% is white, and it tracks perceived brightness far better than HSL.
  • C (chroma): how colorful it is, from 0 (gray) upward. Practical values rarely go above about 0.37.
  • H (hue): an angle from 0 to 360, though the angles don't line up with HSL hues.

Our indigo in OKLCH is about oklch(51% 0.23 277). Both oklch() and oklab() are supported in all current major browsers.

Why perceptual uniformity matters

When lightness means what it says, palette building becomes predictable. Keep L fixed and change only the hue, and you get a set of colors with similar visual weight, so text contrast behaves similarly across all of them. Step L down in equal amounts, and you get shades that look evenly spaced instead of bunching up in the dark end. Gradients interpolated in OKLCH or OKLab also tend to avoid the muddy gray middle that sRGB gradients often show.

OKLCH can also describe colors outside sRGB, such as the more saturated greens and reds that wide-gamut (Display P3) screens can show. On a standard screen, the browser maps those colors back into the displayable range. Be aware that high chroma values at some hues are out of gamut and will be clipped or adjusted.

Quick comparison

FormatExample (same indigo)Alpha syntaxBest for
HEX#4f46e5#4f46e580Copying from design tools, compact tokens
rgb()rgb(79 70 229)rgb(79 70 229 / 0.5)Working with channel values in JavaScript
hsl()hsl(243 75% 59%)hsl(243 75% 59% / 0.5)Quick manual tweaks
hwb()hwb(243 27% 10%)hwb(243 27% 10% / 0.5)Tint/shade thinking (rarely used)
oklch()oklch(51% 0.23 277)oklch(51% 0.23 277 / 0.5)Design systems, palettes, gradients

Converted values are rounded, so tiny differences in the last digit between tools are normal.

Mixing colors with color-mix()

color-mix() blends two colors in a color space you name. color-mix(in oklch, var(--brand) 15%, white) gives you a soft tint of the brand color, and mixing with transparent gives you a see-through version without converting to a new format. It is supported in all current major browsers. The color space you pick changes the result: mixing in srgb can look duller in the middle, while oklch or oklab usually keeps the blend vivid.

Worked example: a small brand palette

Say your brand color arrives from a designer as #4f46e5. Here is a practical way to build hover, soft-background and border variants from it:

  1. Convert it once. #4f46e5 is roughly oklch(51% 0.23 277).
  2. For hover, keep chroma and hue and lower lightness by about 6 points to oklch(45% 0.23 277). Because L is perceptual, the change looks the same size it would for any other hue.
  3. For a soft background, mix 15% of the brand into white with color-mix().
  4. For a border, mix the brand with transparent instead of white.
:root {
  --brand: oklch(51% 0.23 277);
  --brand-hover: oklch(45% 0.23 277);   /* same hue, a bit darker */
  --brand-soft: color-mix(in oklch, var(--brand) 15%, white);
  --brand-border: color-mix(in oklch, var(--brand) 40%, transparent);
}

Then check the pairs that carry text. White text on the brand color and brand-colored text on the soft background both need enough contrast. Run them through the color contrast checker, and see the WCAG contrast requirements guide for the ratios that apply to normal text, large text and UI components.

Fallbacks and browser support

Every format covered here works in current versions of Chrome, Edge, Firefox and Safari. If your audience includes very old browsers, use the cascade: declare a hex value first and the modern value second. Browsers that don't understand the second line ignore it and keep the first.

.button {
  background: #4f46e5;              /* fallback for very old browsers */
  background: oklch(51% 0.23 277);  /* modern browsers use this one */
}

Newer features such as relative color syntax (deriving one color from another inside a color function) have arrived more recently, so check current support on MDN's color value reference before relying on them without a fallback. MDN also has a detailed page on oklch().

Which format should you use?

  • Small site, colors copied from a design file: hex is fine. Don't convert for the sake of it.
  • Design system or theme with many shades: define tokens in OKLCH so variants are predictable.
  • Hover, tint and transparency variants: derive them with color-mix() from one source token.
  • Anything with text on it: whatever the format, verify contrast. No color notation guarantees accessibility on its own.

Frequently asked questions

Is HEX or RGB better for CSS?

Neither is better. #4f46e5 and rgb(79 70 229) describe exactly the same color, and browsers treat them identically. Use whichever your team and design tools already use, and stay consistent.

What does the 8-digit hex code mean?

The last two digits are the alpha channel, from 00 (fully transparent) to ff (fully opaque). #4f46e580 is the same indigo at about 50% opacity, because 80 in hex is 128, and 128 / 255 is roughly 0.5.

Can I use oklch() in production?

Yes. oklch() and oklab() are supported in all current major browsers, including Chrome, Edge, Firefox and Safari. If you still need to support much older browsers, declare a hex or rgb() fallback first and the oklch() value after it.

Why does HSL lightness look wrong?

HSL lightness is a simple formula on the RGB values, not a measure of how bright a color looks to people. Yellow and blue at hsl lightness 50% look very different in brightness. OKLCH lightness was designed to track perceived brightness much more closely.

What is color-mix() used for?

color-mix() blends two colors in a color space you choose, for example color-mix(in oklch, blue 70%, white). It is handy for hover states, tints and shades generated from one brand color without hard-coding every variant.

Tools for this guide