Color Format Converter & WCAG Contrast Checker (HEX · rgb · hsl)

Convert one color between HEX, rgb() and hsl() and read its WCAG relative luminance, then test a foreground/background pair against all five WCAG 2.1 contrast thresholds and get the smallest fix when it fails. Everything is computed in your browser.

Part 1 — one color: every notation plus its luminance. Accepted input: #f0a, #FF00AA, FF00AA, rgb(255, 0, 170).

Worked colours

You typeHEX (uppercase)rgb()Relative luminance L
#f0a#FF00AArgb(255, 0, 170)0.2416
rgb(119,119,119)#777777rgb(119, 119, 119)0.1845
#FFFFFF#FFFFFFrgb(255, 255, 255)1.0000
hsl(210, 60%, 40%)———

Part 2 — foreground on background: the five WCAG 2.1 gates, one verdict each. The two parts are independent: a parse error here leaves Part 1 untouched.

The maths behind the numbers

Two jobs, deliberately kept apart

Part 1 answers “what else can I call this color”. A design file says #FF00AA, a CSS custom property wants rgb(), a theme generator works in hsl(), and an accessibility audit wants a luminance number - four spellings of one thing. The parser is forgiving about the useful variations (# optional, three-digit shorthand, pasted rgb(...) with commas or spaces) and strict about everything else, with an error that quotes the offending token: #1a2g89 reports position 4 (whitespace is dropped first, then positions count the digits after the #, or the items inside rgb()).

Part 2 answers a different question, so it gets its own inputs and its own message line. A broken background color there cannot wipe a valid conversion above, and vice versa.

The maths behind the numbers

Luminance. Each sRGB channel is normalised (c = v / 255) and linearised, removing the gamma encoding screens apply:

c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ^ 2.4
L = 0.2126 R + 0.7152 G + 0.0722 B

The weights are why green dominates: the eye is roughly twelve times more sensitive to it than to blue. #00FF00 reaches L = 0.7152, while #0000FF only gets 0.0722.

Ratio. ratio = (max(L1, L2) + 0.05) / (min(L1, L2) + 0.05), displayed to two decimals. The 0.05 models the light that even “black” reflects in a room; it is what caps the scale at 21:1 (#000000 on #FFFFFF) instead of infinity.

Thresholds, from WCAG 2.1. Success criterion 1.4.3 (AA) needs 4.5 for body text and 3 for large text, where “large” means at least 24px, or at least 18.66px at weight 700 or above. 1.4.6 (AAA) needs 7 and 4.5. 1.4.11 (non-text contrast) needs 3 for UI component edges and meaningful graphics. Every row in the table shows the same measured ratio against its own bar.

HSL. Standard conversion; hue is rounded to a whole degree, S and L to one decimal. A neutral grey has no hue by definition, so it is reported as hsl(0, 0.0%, l%).

The repair suggestion. When the ratio falls short of 4.5, one side is scaled toward pure black or pure white, channel by channel and by the same fraction (v ← v + (target − v) × i/255), one 255th per step, and whichever of the four directions passes in the fewest steps is reported. Proportional scaling is what keeps hue and saturation intact - subtracting a fixed amount would quietly desaturate. For #777777 on white, two steps give #767676, the gray that finally clears 4.5:1 at 4.54:1.

Where this can mislead you

  • It assumes your screen is sRGB and uncalibrated-but-correct. A wide-gamut phone, a “warm” night-light profile, a color-managed browser, or a monitor with a custom ICC curve means the pixels you are looking at are not 119/255. The arithmetic is exact; the color in front of you may not be.
  • Contrast is a luminance ratio, not legibility and not taste. Font weight, x-height, anti-aliasing, letter-spacing, texture behind the text, and how tired the reader is all affect readability and none of them are in the formula. AA is a floor negotiated in 1990s-ish vision research terms, not a comfort setting.
  • Hue confusion is invisible to it. Red carries weight 0.2126, so #FF0000 on white scores 4.00:1 while a same-lightness green sails past 7:1 - and #FF0000 on black is 5.25:1. If color is the only thing distinguishing states, add an icon or a label.
  • Transparency is refused on purpose. rgba(), 4- and 8-digit hex are rejected because an alpha-bearing color has no single contrast value: composite it onto its real background first, then paste the result.
  • Rounded display, unrounded verdict. 4.497 shows as 4.50:1 and still reads Fail. Near a boundary, treat it as failing.

Questions

Why won’t it accept red or hsl(120, 50%, 50%) as input? Because “which red” is a spec-version question (CSS4 red is #FF0000, but rebeccapurple and color-mix() open a can of parsing), and pointing at one character beats guessing. Write hex or rgb(); hsl() is produced for you.

Which gray is safe on white? Anything at #767676 or darker. #777777 - a number people paste from old stylesheets constantly - is 4.48:1 and fails body AA by a hair.

Does what I type go anywhere? It stays in the tab: parsing, linearisation and the ratio are all in the script, and none of them ships the colors you enter off to a server, so they never leave this device.

Support on PayPal