HEX, RGB, HSL and OKLCH: CSS Colour Formats Explained

By , founder of Softaware Commerce · Published · Updated

Drafted with AI assistance. Every command and code example was run and its output checked before publication. How guides are made

CSS lets you write the same colour in several notations: #4f46e5, rgb(79 70 229), hsl(243.4 75.4% 58.6%) and oklch(0.511 0.23 276.97) all describe the same indigo. HEX, rgb(), hsl() and hwb() are different ways of writing sRGB values, so converting between them is exact arithmetic. lab(), lch(), oklab() and oklch() are based on how people perceive colour and can describe colours outside sRGB. Use HEX or rgb() for values copied from a design file, and oklch() when you need lightness steps that look even.

Which colour formats does CSS support?

All the formats below are defined in CSS Color Module Level 4. Every value in the second column was parsed with the culori 4 colour library and converts back to #4f46e5.

FormatThe same indigoComponentsGood for
HEX#4f46e5Red, green, blue in base 16 (00–ff)Copying from design tools; compact
rgb()rgb(79 70 229)0–255 or 0%–100% per channelReading channel values; adding alpha
hsl()hsl(243.4 75.4% 58.6%)Hue angle, saturation, lightnessQuick manual tweaks
hwb()hwb(243.4 27.5% 10.2%)Hue, whiteness, blacknessMixing a hue with white or black
lab() / lch()lab(39.38 41.55 -80.97)CIE lightness plus a/b or chroma/huePrint and colour-science workflows
oklab() / oklch()oklch(0.511 0.23 276.97)Perceptual lightness, chroma, huePalettes, gradients, accessible scales
Namednone for this colour148 fixed keywordsPrototypes, readable defaults

The Color Converter on this site converts between HEX, RGB, HSL and CMYK. It does not read or write hwb(), lab(), lch(), oklab() or oklch().

How do 3-, 4-, 6- and 8-digit HEX colours work?

A hex colour is pairs of hexadecimal digits for red, green and blue, each from 00 (0) to ff (255). An optional fourth pair is alpha. The short forms repeat each digit:

DigitsExampleExpands toMeaning
3#f0c#ff00ccrgb(255 0 204)
4#f0c8#ff00cc88Same colour, alpha 0x88 ÷ 255 ≈ 0.533
6#4f46e5(full form)rgb(79 70 229)
8#4f46e5cc(full form)rgb(79 70 229 / 0.8)

Alpha in HEX is a byte, not a percentage, so 50% opacity has no exact value: 80 is 128 ÷ 255 ≈ 0.502. Values that do divide evenly include cc (0.8), 33 (0.2) and 99 (0.6). Case does not matter, and only colours whose pairs repeat (such as #ff00cc) have a short form.

What is the modern rgb() syntax?

CSS Color 4 introduced a space-separated syntax with alpha after a slash, and made rgba() an alias of rgb(). These are equivalent:

color: rgb(79 70 229 / 80%);
color: rgb(79 70 229 / 0.8);
color: rgba(79, 70, 229, 0.8);   /* legacy comma syntax */
color: #4f46e5cc;

In the modern syntax you can mix numbers and percentages, and use none for a missing component; in the legacy comma syntax you cannot, and you cannot mix commas with spaces. The same rules apply to hsl()/hsla(). The Color Converter accepts both comma and space forms of rgb() and hsl() including percentage channels such as rgb(31% 27.5% 89.8%) and hues in deg, turn, rad or grad, but it ignores alpha.

How do I convert HEX to RGB and HSL by hand?

HEX to RGB

Split the six digits into pairs and read each pair as a base-16 number: 4f = 4 × 16 + 15 = 79, 46 = 70, e5 = 14 × 16 + 5 = 229. The Number Base Converter can do the arithmetic. In JavaScript:

function hexToRgb(hex) {
  let h = hex.replace('#', '');
  if (h.length <= 4) h = [...h].map((c) => c + c).join('');
  const n = (i) => parseInt(h.slice(i, i + 2), 16);
  return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) / 255 : 1 };
}
hexToRgb('#4f46e5'); // { r: 79, g: 70, b: 229, a: 1 }
hexToRgb('#f0c8');   // { r: 255, g: 0, b: 204, a: 0.5333333333333333 }

RGB to HSL

  1. Scale each channel to 0–1: r = 79/255 = 0.3098, g = 0.2745, b = 0.8980.
  2. Find max = 0.8980 (blue) and min = 0.2745, so the difference d = 0.6235.
  3. Lightness L = (max + min) ÷ 2 = 0.5863, or 58.6%.
  4. Saturation S = d ÷ (1 − |2L − 1|) = 0.6235 ÷ 0.8275 = 0.7536, or 75.4%.
  5. Hue: because blue is the maximum, H = 60° × ((r − g) ÷ d + 4) = 60° × (0.0566 + 4) = 243.4°. (If red is the maximum, use (g − b) ÷ d, adding 6 if negative; if green, use (b − r) ÷ d + 2.)

So #4f46e5 is hsl(243.4 75.4% 58.6%). The Color Converter rounds HSL to whole numbers and shows hsl(243, 75%, 59%); converting that rounded value back gives #5048e5, not #4f46e5. Keep the HEX or RGB value as your source of truth and treat rounded HSL as an approximation.

The converter's CMYK output uses the simple formula K = 1 − max(r, g, b), then C = (1 − r − K) ÷ (1 − K) and so on, giving cmyk(66%, 69%, 0%, 10%) for this colour. Real print conversion depends on the printer's colour profile, so treat it as a starting point.

Why is HSL lightness not perceptual?

HSL lightness is the average of the largest and smallest channel, which has nothing to do with how bright a colour looks. The CSS Color 4 specification gives the example of hsl(240deg 100% 50%) (blue) and hsl(60deg 100% 50%) (yellow): both have 50% lightness, yet the yellow looks much lighter. The numbers agree with your eyes in other models:

ColourHSL lightnessOKLCH lightnessContrast with black (WCAG)
Blue #0000ff50%0.4522.44:1
Green #00ff0050%0.86615.30:1
Yellow #ffff0050%0.96819.56:1

HSL hue is also uneven: some 30° steps barely change the colour while others jump. In practice, a palette built by changing only HSL lightness (as the Color Picker's tints and shades do, in steps of 8 percentage points) can look uneven across hues, and text contrast cannot be predicted from HSL values.

What are lab(), lch(), oklab() and oklch()?

CIE Lab has a lightness L (0–100) and two opponent axes: a (green to red) and b (blue to yellow). LCH is the same space in polar form: lightness, chroma (colourfulness) and hue angle. Oklab, published by Björn Ottosson in 2020, follows the same idea with better hue uniformity; in his comparison, blues in CIE Lab shift towards purple as they become lighter or less saturated. OKLCH is Oklab in polar form.

oklch(L C H / alpha)
/* L: 0–1 or 0%–100%   C: 0 upwards, 100% = 0.4   H: 0–360 degrees */
color: oklch(0.511 0.23 276.97);
color: oklch(51.1% 0.23 276.97 / 0.5);

The practical benefits of OKLCH:

  • Predictable lightness. Two colours with the same L look about equally light, so a scale like L = 0.95, 0.85 … 0.25 at a fixed chroma and hue gives even steps.
  • Hue stays put. Changing L or C keeps the hue, which makes hover and disabled states easy to derive.
  • Wider gamut. OKLCH can describe colours outside sRGB, for example oklch(0.7 0.25 150), a green that sRGB cannot display. Browsers map such values to the nearest colour the screen can show, so the same value can look more saturated on a wide-gamut display.

hwb() is simpler: hue plus how much white and black to mix in. hwb(243.4 27.5% 10.2%) is the indigo again; it is easy to reason about but has the same perceptual problems as HSL.

Can I use them in browsers today?

According to the web-features data behind Baseline (version 3.40.1), lab()/lch() and oklab()/oklch() became available in all major browsers on 9 May 2023 and reached "widely available" status on 9 November 2025. hwb() has been widely available since October 2024. Gradient interpolation in a chosen colour space (below) has been available in all major browsers since June 2024 but is not yet widely available. If you support older browsers, declare a HEX or rgb() fallback first; a browser that does not understand the second declaration ignores it:

.button {
  background: #4f46e5;
  background: oklch(0.511 0.23 276.97);
}

How does alpha transparency work?

Every modern colour function takes alpha after a slash, as a number from 0 to 1 or a percentage: oklch(0.511 0.23 276.97 / 40%). The keyword transparent is transparent black, rgb(0 0 0 / 0). Alpha in a colour affects only that property, whereas the opacity property makes the whole element, including its text and children, see-through. When you check text contrast, the colour that matters is the result of blending a semi-transparent colour with whatever is behind it.

What about named colours?

CSS defines 148 named colours, from aliceblue to yellowgreen, plus the keywords transparent and currentcolor. The list includes duplicate spellings such as gray and grey, and rebeccapurple (#663399), added in CSS Color 4. The names come from historical lists and are not a designed palette: darkgray (#a9a9a9) is lighter than gray (#808080). They are fine for prototypes; real designs use exact values.

How do I check colour contrast for WCAG?

WCAG 2.2 defines the contrast ratio as (L1 + 0.05) ÷ (L2 + 0.05), where L1 and L2 are the relative luminances of the lighter and darker colour. Relative luminance linearises each sRGB channel and weights them 0.2126 (red), 0.7152 (green) and 0.0722 (blue). Success criterion 1.4.3 (level AA) requires at least 4.5:1 for normal text and 3:1 for large text (at least 18 point, or 14 point bold); level AAA raises these to 7:1 and 4.5:1.

function luminance({ r, g, b }) {
  const lin = (v) => {
    v /= 255;
    return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
  };
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrast(hex1, hex2) {
  const [hi, lo] = [luminance(hexToRgb(hex1)), luminance(hexToRgb(hex2))]
    .sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
}
contrast('#4f46e5', '#ffffff').toFixed(2); // '6.29'
contrast('#767676', '#fff').toFixed(2);    // '4.54'

The indigo on white passes AA for normal text but not AAA. #767676 is the lightest grey that passes 4.5:1 on white; #777777 gives 4.48:1 and fails. None of the site's colour tools calculates contrast, so use this function or your browser's developer tools.

Why does my gradient go grey in the middle?

Gradients between HEX, rgb() or hsl() colours are interpolated in sRGB by default, for compatibility. From blue to yellow, each channel moves halfway, giving #808080 grey at the midpoint. CSS now lets you choose the interpolation colour space:

background: linear-gradient(90deg, blue, yellow);           /* grey middle */
background: linear-gradient(90deg in oklab, blue, yellow);  /* #6cabc7 middle */
background: linear-gradient(90deg in oklch, blue, yellow);  /* through cyan and green */

In Oklab the midpoint is a soft blue (#6cabc7, calculated with culori). In OKLCH the hue travels around the colour wheel, by the shorter route unless you add longer hue, so the middle is a saturated teal; it can fall outside sRGB, in which case the browser maps it to the nearest displayable colour. The CSS Gradient Generator outputs a plain sRGB gradient; add in oklch after the angle yourself if you want it.

Quick reference

  • HEX, rgb(), hsl() and hwb() are all sRGB; converting between them is exact, apart from rounding.
  • Short HEX doubles each digit; the fourth or eighth digit pair is alpha out of 255.
  • Modern syntax: spaces between components, / alpha at the end; rgba() is an alias of rgb().
  • HSL lightness does not match perceived lightness; OKLCH lightness does, closely.
  • Give an sRGB fallback before oklch() if you support browsers from before 2023.
  • Check contrast with the WCAG formula: 4.5:1 for body text, 3:1 for large text at AA.
  • Use in oklab or in oklch to stop gradients going grey.

Frequently asked questions

Is HEX or RGB better for CSS?

Neither is better: browsers treat them identically. HEX is shorter and what most design tools export; rgb() is easier to read channel by channel. For colours that need consistent lightness steps, consider oklch() instead of either.

What is the HEX code for 50% transparency?

There is no exact one. Alpha is stored as a byte from 0 to 255, so 80 (128) is 50.2% and 7f (127) is 49.8%. If you need an exact percentage, write rgb(79 70 229 / 50%).

Why does converting HSL back to HEX give a slightly different colour?

Because the HSL values were rounded. #4f46e5 is exactly hsl(243.4 75.4% 58.6%), but rounded to hsl(243 75% 59%) it converts back to #5048e5. Keep decimals, or keep the original HEX value.

What does the C in OKLCH mean, and how high can it go?

C is chroma, how colourful the colour is; 0 is grey. CSS maps 100% to 0.4. The highest chroma sRGB can show depends on hue and lightness: pure blue #0000ff has a chroma of about 0.313 and pure yellow about 0.211. Values beyond what a screen can show are mapped by the browser.

Can I convert CMYK to HEX accurately?

Only approximately. CMYK values describe ink amounts for a particular printing process; without the printer's colour profile there is no single correct screen colour. The Color Converter uses the simple formula shown above, which is a reasonable starting point for the web.

Tools for this guide