Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

Our most used text colour passes WCAG at 6.29 to 1, and APCA says it is only legible at 42px

Yesterday I ran axe-core against all 79 pages of our site and it reported 981 colour contrast failures. Today I want to look at the other side of that number: the colours axe says are fine. The WCAG 2 contrast ratio is

Yesterday I ran axe-core against all 79 pages of our site and it reported 981 colour contrast failures. Today I want to look at the other side of that number: the colours axe says are fine.

The WCAG 2 contrast ratio is a formula from 2008 that divides two relative luminances and adds 0.05 to each to avoid dividing by zero. It is symmetric, which means it does not care whether your text is light on dark or dark on light, and it has a known weakness in the dark half of the range, where that 0.05 flattens real differences. APCA is the candidate replacement being developed for WCAG 3. It is asymmetric, it accounts for polarity, and it returns a lightness contrast value, Lc, from roughly -108 to 106 instead of a ratio.

Our site is dark. #171717 background, grey text. That is exactly the region where the two methods are supposed to disagree, so I scored every text colour on the site with both.

The instrument

Every element on every page that owns a non-empty text node, with its computed colour and the background it actually sits on. Tailwind opacity modifiers mean the computed color is frequently oklab(... / 0.6), which no regex can read, so each colour goes through a 1x1 canvas:

ctx.fillStyle = getComputedStyle(el).color;   // any CSS colour at all
ctx.fillRect(0, 0, 1, 1);
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;  // sRGB bytes

Then the alpha is composited over the first opaque ancestor background, and the resulting pair is scored twice: once with the WCAG 2 ratio, once with APCAcontrast(sRGBtoY(txt), sRGBtoY(bg)) from apca-w3 0.1.9, copied into the page so nothing is loaded at runtime.

The whole site, 79 pages:

elements carrying text 8,575
characters of visible text 381,241
distinct text colours 12
distinct backgrounds 7
distinct font sizes 15
distinct weights 4 (400, 500, 600, 700)
distinct colour + background + size + weight combinations 52

52 combinations is the entire typographic surface of the site. Every judgement below is one of 52 rows.

What WCAG 2 says

Four of the 52 fail AA, covering 1,120 elements:

text background size elements pages ratio needs
#656565 #171717 14px 1,044 15 3.08 4.5
#727272 #171717 14px 68 34 3.73 4.5
#727272 #171717 12px 4 2 3.73 4.5
#585858 #171717 14px 4 2 2.52 4.5

All four are the same mistake: a Tailwind slash opacity modifier on an already muted grey, text-muted-foreground/60 and /70, which produces a colour nobody chose. The worst one lives on the 15 question category pages at about 68 elements each, and those elements average 4.4 characters, so the most numerous failure on the site is also the one carrying the least text. 1,044 elements, roughly 4,000 characters between them.

My element level count of 1,120 against axe's 981 node count is the two tools counting different things, not a disagreement: axe skips nodes whose background it cannot resolve and nodes it judges to have no visible text. Same two colours, either way.

What APCA says

Using the APCA guidance tiers of Lc 75 for body text, Lc 60 from 24px, and Lc 45 for large or bold text, 20 of the 52 combinations fail, covering 5,603 elements. Breaking that against the WCAG result:

combinations elements
fail both methods 4 1,120
pass WCAG, fail APCA 16 4,483
fail WCAG, pass APCA 0 0

Zero. Every single colour WCAG objects to, APCA also objects to, and APCA then objects to sixteen more. On a dark site the newer model is strictly stricter, and the gap is not marginal.

Here is the one that matters, because it is our most used text colour on 76 pages:

#999999 on #171717
  WCAG 2:  6.29 : 1     pass AA (needs 4.5), pass AAA for large text
  APCA:    Lc 46.2

6.29 is not a squeak past the line, it is 40 per cent above it. And APCA's own font size lookup table, which is part of the specification rather than my interpretation, says this about Lc 45:

Lc min size at weight 400 min size at weight 700
30 108 px 72 px
45 42 px 24 px
60 24 px 16 px
75 18 px 14 px
90 16 px 14 px
100 15 px 13 px

We use #999999 at 12px, 14px, 16px and 18px. 2,753 elements of it at 14px alone. APCA's position is that this colour is a headline colour, usable at 42px and weight 400, and that everything smaller is decoration.

The size of the correction

If you accept APCA's body text tier, here is the grey you need on a #171717 background, with what WCAG 2 would have said about each:

target grey WCAG 2 ratio
Lc 45 #979797 6.14 : 1
Lc 60 #b3b3b3 8.55 : 1
Lc 75 #cdcdcd 11.28 : 1
Lc 90 #e5e5e5 14.23 : 1

To satisfy APCA for ordinary paragraph text on this background, you need a colour WCAG 2 rates at 11.28 to 1. Our current grey rates 6.29. Both numbers are "pass AA". The entire distance between a muted grey that reads and one that does not is invisible to the ratio, because above about 5 to 1 on a dark background the WCAG scale has stopped resolving.

The reverse direction is also worth seeing, because it stops this being a story about APCA simply being harsh. Our three legal pages are light themed, and there the two methods nearly agree:

#64748b on #ffffff at 12px
  WCAG 2:  4.76 : 1   pass AA
  APCA:    Lc 73.0    needs 75, so a near miss

Dark text on white, a 4.76 ratio, and APCA calls it marginal rather than wrong. The disagreement is not a constant offset, it is specifically about light text on dark backgrounds, which is the mode most products built in the last five years ship by default.

What I am changing

  1. Delete the /60 and /70 opacity modifiers. They produce four colours nobody picked and they are the only thing on the site that fails the standard we are actually held to. One find and replace.
  2. Move --muted-foreground from #999999 to about #b3b3b3 and see what it costs visually. That is Lc 60, which covers us at 24px and leaves 14px body text still short of the body tier, so it is a step rather than a fix.
  3. For the genuinely small text, 12px labels and metadata lines, raise the colour to near full brightness or raise the size. APCA's view is that a 12px muted grey is not a design decision, it is a decision not to be read, and having now seen the table I find that hard to argue with.
  4. Keep scoring both ways. WCAG 2 is the one that gets audited, so it stays the gate. APCA is the one that matches what I see when I squint at my own footer at night.

If you want to run this on your own dark site, the whole thing is about 40 lines: apca-w3 from npm, a 1x1 canvas for the colour parsing, and one walk of the DOM. Then open any page of ours, the FAQ is representative, and compare the two numbers for your own muted grey. The gap between them is the part of your palette that the 2008 formula cannot see.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.