CSS

HEX vs RGB vs HSL: Color Codes Explained, with WCAG Contrast

2026-10-13 · 9 min read · Convert colors free →

Every front-end project I have worked on ends up with the same argument in code review: should this colour be #22d3ee, rgb(34 211 238) or hsl(188 86% 53%)? All three describe the same colour. HEX, rgb() and hsl() are three notations for colours in the sRGB colour space, and a browser converts them to the same pixel values. The difference is how easy each one is to read, edit and reason about.

This guide explains each format, works through the conversion maths with a real example, and then covers the part users notice: whether your text colour has enough contrast to be readable. Every contrast figure below comes from the WCAG formula, not an estimate.

HEX: Compact and Copy-Paste Friendly

A hex colour is a # followed by hexadecimal digits. MDN lists four valid forms:

FormExampleMeaning
#RRGGBB#ff0099Red, green, blue, two digits each (00–ff)
#RGB#f09Shorthand: each digit is doubled, so #f09 = #ff0099
#RRGGBBAA#ff009980Adds alpha (transparency)
#RGBA#f098Shorthand with alpha, each digit doubled

Hex is case-insensitive, so #00FF00 and #00ff00 are identical. For alpha, 00 is fully transparent and ff is fully opaque. People often ask what 80 means: it is 128 out of 255, or 50.2% opacity, not exactly 50%. MDN marks hex colours, as a feature, as widely available across browsers since July 2015.

Design tools export hex and most style guides list it, so it is the best format for storing brand colours. Its weakness: you cannot look at #22d3ee and know how to make it 10% darker.

RGB: The Channels Spelled Out

rgb() gives the same three channels as decimal numbers from 0 to 255 (or percentages). Modern CSS uses spaces between values and a slash before alpha; the older comma syntax still works:

color: rgb(34 211 238);          /* modern */
color: rgb(34 211 238 / 50%);    /* modern, with alpha */
color: rgb(34, 211, 238);        /* legacy comma syntax */
color: rgba(34, 211, 238, 0.5);  /* rgba() is an alias of rgb() */

According to MDN, rgba() is now exactly equivalent to rgb(), so there is no reason to switch function names to add transparency. One catch in the legacy syntax: the three channels must be all numbers or all percentages, not a mix.

Converting HEX to RGB by hand

Each pair of hex digits is one channel in base 16. Take ToolPry's accent colour, #22d3ee:

22 → 2×16 + 2  = 34   (red)
d3 → 13×16 + 3 = 211  (green)
ee → 14×16 + 14 = 238 (blue)

#22d3ee = rgb(34 211 238)

To go the other way, divide each channel by 16. The quotient is the first digit and the remainder the second. In JavaScript, (211).toString(16) returns "d3" and parseInt("d3", 16) returns 211.

HSL: The Format You Can Edit in Your Head

hsl() describes the same sRGB colour as hue, saturation and lightness:

  • Hue is an angle on the colour wheel. 0deg and 360deg are the same red; 120 is green, 240 is blue. Adding 180 degrees gives the complementary colour.
  • Saturation runs from 0% (grey) to 100% (full colour).
  • Lightness runs from 0% (black) through 50% (the pure colour) to 100% (white).
color: hsl(188deg 86% 53%);         /* ≈ #22d3ee */
color: hsl(188deg 86% 53% / 60%);   /* with alpha */
color: hsl(188, 86%, 53%);          /* legacy syntax */

This is why I reach for HSL when building a palette. A hover state is the same hue with lightness lowered by 8 points; a muted background is the same hue with less saturation. The conversion is rounded, though: #22d3ee is exactly hue 187.9°, saturation 85.7%, lightness 53.3%, which displays as hsl(188 86% 53%). Converting the rounded value back can land slightly off: hsl(188 86% 53%) gives #20d3ee, not #22d3ee. Keep the hex as your source of truth.

HSL's big limitation: lightness is not brightness

HSL lightness does not match how bright a colour looks. Pure yellow hsl(60 100% 50%) and pure blue hsl(240 100% 50%) both have 50% lightness, yet their WCAG relative luminance is 0.928 and 0.072 respectively. Yellow has nearly thirteen times the relative luminance of blue. So a palette built by fixing HSL lightness across hues looks uneven, and two colours with the same lightness should never be assumed to have the same contrast. Newer CSS functions such as lch() and oklch() exist to solve this problem.

Which Format Should You Use?

TaskBest formatWhy
Brand colours, design tokensHEXCompact, exported by design tools, easy to diff
TransparencyRGB or HSL with / alphaReadable; 8-digit hex alpha is hard to read
Hover, active, tint and shade variantsHSLChange one number instead of three
Values from JavaScript or canvasRGBPixel data is already 0–255 per channel

Contrast: The Number That Matters

Whatever notation you use, users care about one thing: can they read the text? WCAG 2.2 defines this precisely. First, compute each colour's relative luminance: divide each 8-bit channel by 255, linearise it (divide by 12.92 if it is at most 0.04045, otherwise raise (c + 0.055) / 1.055 to the power 2.4), then combine:

L = 0.2126 × R + 0.7152 × G + 0.0722 × B

contrast = (L_lighter + 0.05) / (L_darker + 0.05)

Ratios run from 1:1 (identical colours) to 21:1 (black on white). The WCAG thresholds are:

RequirementRatio
AA, normal text (SC 1.4.3)at least 4.5:1
AA, large text: 18pt (about 24px) or 14pt bold (about 18.5px)at least 3:1
AAA, normal text (SC 1.4.6)at least 7:1

WCAG is strict here: the ratio is not rounded, so 4.499:1 fails 4.5:1. With mid-greys that matters. Here are figures I calculated with the formula above:

Text colourOn whiteAA normal text?
#7676764.54:1Pass
#7777774.48:1Fail, by one step of grey
#5959597.00:1Pass, and meets AAA
#22d3ee1.81:1Fail; fine on a dark background (10.95:1 on #0a0a0b)

The last row is the lesson I learned the hard way: a colour is never "accessible" on its own. ToolPry's cyan is excellent on a near-black background and unreadable on white. Always check the real pair.

Note on the formula: older versions of the WCAG documents used 0.03928 instead of 0.04045 as the linearisation threshold. The W3C updated it in May 2021 and states that the change has no practical effect on results.

Using ToolPry's Color Converter

The Color Converter keeps HEX, RGB and HSL in sync: type into any of the three fields or use the colour picker, and the other two update. It accepts 3- and 6-digit hex, and both the modern space-separated and the legacy comma syntax for rgb() and hsl(). Below the converter, the contrast checker takes a text and background colour, shows the WCAG ratio, and tells you whether the pair passes AA and AAA for normal-size text. Everything runs in your browser.

For backgrounds, the CSS Gradient Generator builds linear and radial gradients you can paste straight into your stylesheet.

Frequently Asked Questions

Is HEX or RGB better for CSS?

Neither is technically better: both describe the same sRGB colour and render identically. HEX is more compact and is what design tools export, so it suits stored brand colours. rgb() is easier to read when you need transparency, using the modern syntax rgb(34 211 238 / 50%).

How do I convert HEX to RGB?

Split the six digits into three pairs and convert each pair from base 16 to base 10. For #22d3ee: 22 is 34, d3 is 211 and ee is 238, so the result is rgb(34 211 238). A 3-digit hex like #f09 is first expanded by doubling each digit, to #ff0099.

What does the alpha value 80 mean in an 8-digit hex colour?

Hex alpha runs from 00 (fully transparent) to ff (fully opaque). 80 is 128 out of 255, which is about 50.2% opacity.

What contrast ratio does WCAG require for text?

WCAG 2.2 success criterion 1.4.3 (level AA) requires at least 4.5:1 for normal text and 3:1 for large text, defined as at least 18pt or 14pt bold. Level AAA (criterion 1.4.6) requires 7:1 for normal text. Ratios are not rounded, so 4.499:1 does not pass 4.5:1.

Why do two colours with the same HSL lightness look different in brightness?

HSL lightness is not perceived brightness. Pure yellow and pure blue both have 50% HSL lightness, but their WCAG relative luminance is about 0.93 and 0.07. Use a contrast checker on the actual colour pair instead of relying on HSL lightness.

Sources