Tabpath Research · 17 September 2026 · A correction to our own work
Our scanner reported 118 colour-contrast failures on a storefront that has five. The other 113 were text it measured while the text was still fading in.
This is a correction. We built a scanner, pointed it at 921 e-commerce storefronts, 882 of which returned usable results, ranked them by how badly they failed colour contrast, and started emailing the worst offenders free patches. Then we looked closely at one of them and found the site was fine. The scanner was wrong.
We are publishing the mechanism because it is not our bug. It is a property of how an automated scanner measures a page at a moment in time, and it silently inflates contrast numbers on a large fraction of modern storefronts. If you have ever run a scan, printed the number, and forwarded it to a developer, some part of that number was probably not real.
kadenspaws.com is a Shopify store selling dog supplements. Our scan reported 135 WCAG failures, 118 of them colour contrast. That put it near the top of our shortlist, so we ran our contrast repair over it and prepared an email carrying the patch.
Before sending, we read the individual findings rather than the total. They looked like this:
#fdfdfd on #ffffff 1.01:1 38px em:nth-child(2) > strong #244338 on #234237 1.01:1 36px .image-with-text__heading #c4c0b6 on #faf5ea 1.67:1 20px .text-feature__item .h4
Near-white on white. Dark green on the same dark green. Nobody designs that. So we loaded the live page and read the computed styles off the actual elements:
.vertical-scroll-item.h0 color: rgb(35, 66, 55) on rgb(255,255,255) .image-with-text__heading color: rgb(255,255,255) on rgb(35, 66, 55) .text-feature__item .h4 color: rgb(35, 66, 55) on rgb(250,245,234)
Dark green on white, at roughly 11:1. White on dark green. Perfectly legible, comfortably past the 4.5:1 threshold. The site was never failing. We were measuring it wrong.
Modern themes fade content in as you scroll. An element below the fold sits parked at its initial animation state — typically a low opacity — waiting for an IntersectionObserver to fire and reveal it. We should be precise about how we know: we never caught an element mid-fade. The mechanism is inferred from the blended foreground colours axe reports, which are what a partly transparent element resolves to.
A scanner loads the page, waits a few seconds for scripts to settle, and runs. At that moment every element below the first screenful is still parked. And axe-core, correctly and by design, accounts for opacity when it computes contrast: it blends the element's colour toward whatever is behind it before measuring. Text at opacity: 0.01 over white is measured as almost exactly white — against white.
#234237 on #ffffff. Passes AA with room to spare.
opacity: .01, mid-reveal. Blended, it computes as #fdfdfd on #ffffff.
Both panels contain the identical heading, in the identical colour. The second is rendered at the opacity the scanner caught it at. This is not a rendering trick for the article — it is the same CSS property the theme animates.
The giveaway is the ratio itself. A contrast ratio below about 1.1 means the text and its background are within a couple of RGB steps of each other. That is not a design mistake anybody ships. A ratio under 1.1 is a measurement, not a defect.
One site proves nothing. So we measured the same URL four ways on the same browser build, minutes apart.
| Condition | All WCAG | Contrast | of which ratio < 1.1 |
|---|---|---|---|
| Load, settle, scan — no scroll (what we shipped) | 135 | 118 | 107 |
| Scroll the whole page first, then scan | 23 | 5 | 0 |
Emulate prefers-reduced-motion | 32 | 15 | 0 |
| Both | 22 | 5 | 0 |
Scrolling the page before scanning removes 113 of the 118 reported contrast failures and every single sub-1.1 ratio. Reduced-motion emulation also removed every sub-1.1 ratio, but left 15 contrast findings against the scroll pass’s 5. We have not established what those extra 10 are, so we are not going to explain the gap.
If scrolling first simply suppressed findings, it would suppress them everywhere. The interesting result is that it does not. We took eighteen storefronts — the emailable shortlist plus the four we had already sent patches to — and measured each one twice, minutes apart on the same browser build: once the way we had been shipping, once with a full-page scroll before the scan.
| Storefront | Contrast, as shipped | Contrast, scrolled first | Removed | Sub-1.1 ratios in the original |
|---|---|---|---|---|
| kadenspaws.com | 118 | 5 | 113 | 107 |
| sleepshop.ca | 90 | 1 | 89 | 90 |
| luxuryofhomes.com | 88 | 0 | 88 | 88 |
| kitchenshelves.com | 71 | 0 | 71 | 71 |
| atlaseca.com | 70 | 1 | 69 | 70 |
| grupostop.com | 70 | 25 | 45 | 44 |
| thedecorcircle.com | 67 | 25 | 42 | 66 |
| barfgold.de | 65 | 32 | 33 | 46 |
| 8 sites | 639 | 89 | 550 (86%) | 582 |
| Storefront | Contrast, unscrolled pass | Contrast, scrolled first | Change | Sub-1.1 ratios in the original |
|---|---|---|---|---|
| tigerfitness.com | 182 | 184 | +2 | 0 |
| betinababy.com | 156 | 156 | 0 | 0 |
| animax.ro | 129 | 126 | −3 | 0 |
| rustorange.com | 93 | 93 | 0 | 0 |
| feedpetaluma.com | 74 | 74 | 0 | 0 |
| jennaraecakes.com | 66 | 65 | −1 | 0 |
| licorice.com | 63 | 63 | 0 | 0 |
| magnoliamorango.com | 62 | 62 | 0 | 0 |
| nutriment.co.uk | 4 | 4 | 0 | 0 |
| keeperstop.com | 0 | 0 | 0 | 0 |
| 10 sites | 829 | 827 | −2 (0.2%) | 0 |
Two control rows need a caveat we would rather state than bury: keeperstop.com and nutriment.co.uk reported 114 and 65 contrast failures in the census a few days earlier, and measure 0 and 4 in the unscrolled pass above. That drift is not scroll-reveal — those sites changed, or the earlier scan caught something we cannot reconstruct. They enter the control totals at the lower figures. The correction is surgical, not a blanket suppression. On ten of the eighteen storefronts it moved the total by two failures across 829 — noise. On the other eight it removed 86% of everything reported. The column that separates the two groups perfectly is the last one.
Across the whole sample, every site with a sub-1.1 contrast ratio in its findings was affected, and no site without one was. If your scan report contains ratios near 1.0, it is measuring animations. If it does not, your numbers are probably sound.
Across our 882-storefront census, 632 sites reported at least one contrast failure. Of those, 107 — nearly 17% — show at least one sub-1.1 ratio in the findings we retained. Those 107 sites account for 3,284 of the 10,998 contrast instances in the whole census, about 30%. That is the total reported on affected sites, not an estimate of how many of those instances are phantoms — kadenspaws alone contributes 114 of it, of which this article establishes that 5 are real.
That is a sample-based estimate and it is biased low, for a specific reason: we kept only a handful of example findings per rule per site, and those examples skew toward elements early in the document — which is to say above the fold, which is to say exactly where the artifact does not occur. The true figure is higher. We are not going to guess by how much; we are re-running the census through the corrected method instead.
We were not just reporting inflated numbers. We were ranking by them.
Our merchant shortlist selected storefronts where colour contrast made up 60% or more of all WCAG failures, on the reasonable theory that those are the sites a contrast repair can help most. But a high contrast share is precisely the signature of this artifact — an affected site reports a wall of contrast failures and little else. The filter that was supposed to find our best prospects was selecting for our worst measurements. Of the fourteen emailable stores on that shortlist, eight were phantoms.
If you rank anything by a metric, check that the metric's failure mode does not correlate with the top of your ranking. Ours did, perfectly, and it took an unrelated moment of suspicion to notice.
Take any storefront whose theme fades content in on scroll. Run your scanner twice.
// 1. what most scanners do
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(8000);
const before = await new AxeBuilder({ page }).withTags(TAGS).analyze();
// 2. the same page, revealed first
await page.evaluate(async () => {
const step = Math.max(200, window.innerHeight * 0.8);
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(r => setTimeout(r, 350));
}
window.scrollTo(0, 0);
await new Promise(r => setTimeout(r, 1200));
});
await page.waitForTimeout(3000);
const after = await new AxeBuilder({ page }).withTags(TAGS).analyze();
Then count how many findings in before carry a contrast ratio below 1.1. That number is your over-report.
Our scanner now walks the full page height before axe runs, so every observer fires and every reveal finishes. It costs between about five and nine seconds per scan depending on how long the page is — that is derived from the step size and the waits in the code, not from a timing run. The four storefronts we had already emailed patches to were all in the control group, so those numbers stood and nothing had to be retracted — which was luck, not diligence.
Open the individual findings, not the summary. Sort by contrast ratio. Anything below about 1.1 is almost certainly text your visitors read without difficulty, measured while it was invisible. Subtract those before you brief anybody, quote anybody, or budget anything — and be a little more suspicious of the total than the tool invites you to be.
We are re-running our full census through the corrected method. When it finishes we will publish the corrected prevalence figure here, including if it is less interesting than the one we published before.