Guide Developer Tools 6 min read

Most hex colours change after a round trip through whole-number HSL

Hex, hsl() and CMYK are different ways of writing a screen colour, and rounding to whole numbers between them changes most colours. Contrast against white and black follows from WCAG's formula, which fixes how far any colour can fall short.

Maya Lin
Digital Platforms Analyst
Published 4 Nov 2026, 8:27 PM (SGT)
Share:
A fan of paint colour swatches beside a handful of paintbrushes. A fan of paint colour swatches beside a handful of paintbrushes. Photo by stux on Pixabay
Advertisement

A colour code is one screen colour written as text, and CSS accepts the same colour in several notations: #FF5733 in hex or hsl(11, 100%, 60%). Rounded to whole degrees and percentages, the two stop being interchangeable. On 4 November 2026 we sent all 16,777,216 colours that six-digit hex can name through whole-number hsl() and back. Only 1,775,554 returned unchanged, about one in nine. The rest moved by up to five steps on a red, green or blue channel.

Whole-number hsl() brings back about 11% of hex colours unchanged

This isn't a new problem. Mozilla's bug 372783, opened in 2007, carries a 2009 comment noting that integer HSL "only allows 3.6 million colors" against 16.8 million RGB ones. A 2017 Stack Overflow answer counted 3,518,202 distinct integer HSL triples and advised keeping floating-point values internally.

Our sweep quantifies the problem precisely. The 3,672,360 whole-number inputs land on only 2,037,899 distinct colours, 12.15% of the cube, because greys collapse and many triples round to the same byte. We ran every sweep through the conversion code behind our colour code converter.

RouteReturned exactly, of 16,777,216Largest channel error
hex → whole-number hsl() → hex1,775,554 (10.58%)5
hex → whole-number HSB → hex2,044,773 (12.19%)3
hex → whole-percent naive CMYK → hex2,136,545 (12.73%)2

Type #FF5733 into the converter's HEX field and the HSL(A) field reads hsl(11, 100%, 60%). Type that same hsl() text into the HSL(A) field and the HEX field reads #FF5833, one step higher on green. CSS accepts decimals in hsl(), so the loss belongs to the whole-number habit rather than to HSL itself.

Profile-free cmyk() differs visibly from both press profiles in the W3C's examples

CSS Color Module Level 5, a W3C Working Draft dated 29 October 2026, prints a naive formula, the one our converter also applies: black is one minus the largest of red, green and blue, and each ink takes what remains. Its own example turns one value, device-cmyk(0.091777 0.043303 0.312816 0), into three colours. The SWOP 2006 coated and FOGRA39 press profiles differ by a colour difference (ΔE2000) of 8.17, "clearly visible"; the naive result against SWOP gives 14.3, "very visible". On the draft's scale, 1 or more "is just visible, while 5 or more is just a different color altogether".

Our ΔE2000 code reproduced the 8.17, gave 14.25 for the 14.3, then compared naive results with three more profile values the draft prints.

CMYK (%)Naive sRGBProfile in the draftΔE2000, our arithmetic
0 / 81 / 81 / 30rgb(179, 34, 34)FOGRA395.85
0 / 70 / 20 / 0rgb(255, 77, 204)SWOP 2006 coated14.27
90 / 0 / 90 / 0rgb(25, 255, 25)FOGRA39, outside sRGB27.38

Profiles do not always disagree badly: the draft gives 0/70/20/0 under both, and those two values sit 1.11 apart on our arithmetic.

Naive cmyk() never mixes cyan, magenta and yellow together

The formula always moves the grey part of a colour into black. Across all 16,777,216 colours, no naive result uses cyan, magenta and yellow at once. The draft explains why these numbers describe no particular press: the naive conversion "has no knowledge of the colorimetry of the inks, the dot gain, the colorimetry of the RGB space, and so on." The draft's remedy is a colour profile, though it marks @color-profile and device-cmyk() as at risk. Our ΔE figures above describe the draft's examples only, never any printer.

Advertisement

Each #rrggbb colour reaches AA on white or black, and 38.6% miss AAA on both

WCAG 2.2 defines contrast as (L1 + 0.05) / (L2 + 0.05), with L the relative luminance, and its Success Criteria 1.4.3 and 1.4.6 ask for 4.5:1 for text below large size at AA and 7:1 at AAA. Multiply a colour's ratio against white by its ratio against black and the luminance cancels, leaving 21. The better of the two is therefore never below √21, about 4.58, for any opaque colour. Mark Ransom derived the crossover, a luminance near 0.179, in a Stack Overflow answer of 2010.

On our sweep, 6,482,445 of the 16,777,216 colours (38.64%) reach 7:1 against neither white nor black: those whose relative luminance lies strictly between 0.1 and 0.3. By the same product, that set is exactly the one clearing 3:1 against both. CSS Color 5 adds that "sufficient color contrast is only one piece of the puzzle". Colour vision is a separate question; see our guide to colour-blindness simulation models.

A ratio printed to two decimals can show a pass the verdict refuses

W3C's Understanding document for SC 1.4.3, informative guidance rather than part of the standard, says "the computed values should not be rounded (e.g., 4.499:1 would not meet the 4.5:1 threshold)". A checker that grades the unrounded value but prints two decimals will still display a threshold it has refused. Of 33,554,432 pairs, every 8-bit colour against white and against black, 17,917 print as 4.50 while falling below 4.5, and 22,806 print as 3.00 below 3. #006BFB on black is one: its ratio is 4.49608.

The Understanding document says nothing about transparency, so we chose a method: blend the colour onto the background, round to the byte an 8-bit screen shows, then grade that. #FF5733 at 50% alpha over white becomes #FFAB99, 1.82:1, a fail, against 3.15:1 if graded as opaque. Before a revision dated 4 November 2026, our converter printed two decimals and graded a translucent colour as opaque; the revised version prints 4.496 for #006BFB on black and grades the blended colour.

display-p3 and oklch() reach far beyond what an sRGB screen can show

CSS Color Module Level 4, a Candidate Recommendation Draft of 3 November 2026, defines lab(), oklch() and display-p3 alongside hex and hsl(), and says a valid colour outside what a device can produce "is said to be out of gamut". With the draft's own matrices, we found just under half of the 8-bit display-p3 cube outside sRGB, allowing a small rounding tolerance. We also stepped oklch() through lightness in 1% steps, chroma from 0 to 0.40 in steps of 0.01 and every whole-degree hue, 1,490,760 values in all. With the same tolerance, 423,852 (28.43%) land inside sRGB; with none, 27.77%. CSS Color 5's FOGRA39 green from the table above is outside sRGB yet, in that draft's words, "inside the display-p3 gamut".

Large text in WCAG is set in points, with its own note for CJK type

The lower thresholds, 3:1 at AA and 4.5:1 at AAA, apply to large-scale text, which WCAG 2.2 defines as "at least 18 point or 14 point bold or font size that would yield equivalent size for Chinese, Japanese and Korean (CJK) fonts". It gives no separate size for CJK type. W3C's Understanding document converts the Latin sizes at 1pt = 1.333px, "approximately 18.5px and 24px".

In Singapore, the Ministry of Digital Development and Information's reply to a parliamentary question, dated 12 November 2024, "MDDI's response to PQ on Ensuring High Traffic Government Websites Conform To WCAG", states that the Government "plans to make all high-traffic Government websites accessible by 2030". Its scope is high-traffic Government websites alone.

Advertisement
Maya Lin
Digital Platforms Analyst

Maya Lin covers SaaS platforms, workflow automation, creator tools, and productivity software for RECATOOLS.

View author profile → · Editorial policy

About this byline Maya Lin is a RECATOOLS editorial persona used for platform and productivity coverage. Articles are produced and reviewed under RECATOOLS editorial supervision.

Corrections policy

Advertisement