← The Journal
Craft

No colour can rescue faded text

By Mark5 min read
Blue and yellow ink billowing through clear water against a pale grey background
Photo, MART PRODUCTION / Pexels

A card on this site was faded with CSS opacity, and its text failed contrast. A darker colour can't fix that, and the sum shows why. Plus two other faults from the same evening that are classes rather than slips.

The homepage of this site had a card for a product that isn't out yet. I wanted it to sit back a bit behind the ones that are, so the whole card carried opacity: 0.45, faded to a little under half strength, the way a menu greys out the thing you can't pick yet.

On the 30th of July this year, Lighthouse scored the page 91 for accessibility on mobile. Five pieces of text failed its contrast check, and that card was one of them.

The obvious fix is a darker text colour. Pick a stronger one, run the check again, done. One sum says that can't work here, not with a darker grey and not with any colour at all.

Watering down the ink

Contrast is measured between the text and whatever sits directly behind it. WCAG's AA level, the one most sites aim for, asks for at least 4.5:1 for normal-sized text (WCAG 2.2, contrast minimum).

Opacity on a container doesn't change the text's colour, it changes what reaches your eye. At 45%, the browser mixes 45% of the text's colour with 55% of whatever is underneath, and the contrast check runs on that mix.

Think of it as ink and water. The text is the ink, and opacity is how much water you pour in with it. You can buy the blackest ink there is, the stuff that costs more than the pen, and at 45% it's still more than half water.

So I ran the sum with the blackest ink there is. Pure black text at 45% opacity, on the light blue-grey this site uses for cards, comes out at 3.16:1. On a plain white page, which is about as kind a background as text will ever get, it's 3.36:1. Both fail, and there's no blacker ink left to buy.

Run it the other way and black on white needs about 54% opacity before it clears 4.5:1. On this site's card colour it's 57%.

MDN says plainly that contrast is measured on the colours after opacity is applied (MDN, opacity), and the W3C's own test cases use faded black text as a failing example (ACT rule, text has minimum contrast). What I couldn't find spelt out is the ceiling, the point where the water wins and the colour of the ink, any ink, stops mattering.

It was worse than that, as it turned out. The card's title carried its own fade as well, sitting inside the card's, and fades multiply, so the title was watered down and then watered down again. With the card's fade gone, the title's own still measured 1.90:1.

Both fades came off. The card still sits back, it does it now with a "Coming later" badge and a quieter body colour that passes on its own, which is the way to make something look secondary without pouring water on it. Choose the lighter colour itself and check that, rather than fading a darker one.

The little coloured badges were a milder version of the same thing. Brand colour as small text on a 15% wash of that same brand colour, which is ink written on paper already tinted with it, and the tint, a bit sneakily, eats the contrast. The text colour that passed on the plain card reached only 4.25:1 on the badge. The badge keeps the brand colour as its fill now, and the text takes a shifted tone of its own.

The image squashed on every page

The second fault was on every page of the site.

The stylesheet had img { max-width: 100%; }, a line plenty of stylesheets carry, and every image also had width and height attributes, which is good practice, because the browser can reserve the space before the image arrives and the page doesn't jump about.

Put together, without one more line, they fight. On a narrow screen the width shrinks to fit and the height attribute, unhelpfully, stays exactly where it was, so one 1000 by 667 journal cover was being drawn 362 wide and 666 tall.

The fix is height: auto next to the max-width, one rule, and it fixed every page at once.

Each half is right on its own. You add max-width for one reason and the attributes for another, months apart, quite possibly, and neither looks wrong when you read it back. It only breaks where they meet, on a screen narrower than the image, which in practice means a phone.

The third was the site's own name in the top corner, the link back home.

Under 900 pixels wide the word "Considus" is hidden to save room, and the logo mark beside it was already marked as decorative, so screen readers skip it. Which is right, for a logo. With the word gone as well, though, the link had nothing left in it a screen reader could say.

Lighthouse calls this "Links do not have a discernible name". Someone using a screen reader on a phone would hear "link", and nothing else, at the top of every page.

The fix was an aria-label attribute on the link itself, which gives it a name screen readers read out at every width. It went into the code that generates the pages, rather than the page I happened to be looking at, so every page got it at once. Deque's page on the link-name rule Lighthouse runs covers the other ways a link ends up nameless.

The reading I took from the wrong theme

The first time the contrast was measured, it read the colours from the wrong theme. This site has a dark mode and a light mode, and the first pass took its values from dark mode, where everything was fine. Every failure was in light mode.

The second pass measured in the browser, in both themes, one after the other, and that's the one I trust.

That was the same week, very nearly the same evening, that I found out PageSpeed Insights replays a cached result rather than measuring again. Any colour check on this site now says which theme it read, and if it doesn't say, I don't believe it.

Once everything was fixed, Lighthouse came back at 100 for accessibility and 100 for best practices, and the card for the product that isn't out yet still sits at the back, where I wanted it.