We wrote a piece called The Complete Web Accessibility Color Contrast Guide. Then, last weekend, we ran axe-core against our own blog for the first time.
It fails color contrast.
Not a clever failure either. A boring one. The kind we would point out in a client report in the first paragraph.
Why we finally ran the scanner on ourselves
We have been publishing weekly audits of other people's sites for about a month now — a cohort scan of 30 SaaS pricing pages, an axe-core run against AI-generated UI code, and a few smaller pieces in between. At some point it started feeling dishonest that we had never pointed the same tooling at blog.a11yfix.dev itself. So on April 13 we did.
16 pages. axe-core 4.11. WCAG 2.1 AA plus 2.2 AA plus the best-practice tag so landmark rules would fire. Same harness we use for everything else.
The numbers
- 27 violations total across the 16 pages
- 28 DOM nodes affected
- 0 critical
- 14 color-contrast violations flagged as serious (spanning 15 nodes)
- 4 unique rule IDs — the entire result set fits in four rules
- 1 page with zero violations (the homepage, which is the only page on the site we hand-wrote the layout for)
Nothing new in there. No WCAG 2.2 edge case. No novel aria-live disaster. Just basics. If you handed us this report for a client we would call it "low-severity surface sweep, two afternoons of work to clear."
The problem is we are not the client.
The specific one that hurt
On /blog/color-contrast-guide/, axe-core flagged a single element: the privacy note under the newsletter signup form. The text is "No spam. Unsubscribe anytime." rendered in #898a8f on a pale blue #f0f4ff background. Ratio: 3.13:1.
WCAG AA wants 4.5:1 for normal body text. We are a point and a half under.
The honest part is that this almost certainly looked fine on the monitor the component was designed on. A calibrated sRGB desktop, afternoon light, eyes that are used to reading grays on grays. We have all shipped that pixel. The less honest part is that we wrote an entire guide explaining why "looks fine to me" is not how you make this call, and then shipped a component where that was apparently exactly how we made the call.
One line fix. #898a8f becomes something in the #5a5b60 range and the ratio clears 4.5:1 comfortably. It is not a design problem, it is a default-gray problem, which is the most common failure mode we find in other people's audits too.
The pattern
Here is the part that is actually worth writing about, because it kept showing up in our own report the way it kept showing up in everyone else's.
We did not ship 27 different mistakes in 27 different articles. We shipped two mistakes, in two shared components, and those components got replicated across the site.
The newsletter signup with the .privacy-note text ships on almost every article. One bad hex value, 14 pages tainted, accounting for 14 of the 15 color-contrast failures. The 15th is a syntax-highlighted comment token from the Shiki code theme on the email marketing post — a third-party default we inherited, still our responsibility, but at least not the same bug twice.
The second one is subtler. We have a <aside class="related-resources"> block at the bottom of most articles. It lives inside <main>. axe-core's landmark-complementary-is-top-level rule says an <aside> carries an implicit complementary landmark, and complementary landmarks are supposed to sit next to <main>, not inside it. 11 pages, 11 nodes, one component. Move the aside out of main, or drop its implicit role with role="presentation" on the cases where it really is just a layout container, and that entire column of the report goes to zero.
Two components. Twenty-five of the twenty-seven violations.
If that pattern sounds familiar, it is because we wrote about it earlier this week. In the SaaS pricing pages scan, the single company with the highest DOM-node failure count had 73 nodes affected by what was fundamentally one toggle component repeated across every pricing tier row. We spent a paragraph on how cohort audits overstate the fix surface because shared components inflate node counts. We wrote that observation down, published it, and then discovered that our own blog was a smaller, more embarrassing instance of the exact same thing.
There is a version of this post where we claim we did it on purpose, to make the point cleaner. We did not. We genuinely did not run the scanner on ourselves until last weekend, and when we did, the finding that made us wince was the one that matched the pattern from the article we were proudest of.
The two outliers
Both live on the blog index page — the listing at /blog/, not any individual post.
One: no <h1>. The listing page has <h4> post titles and no heading above them. page-has-heading-one fires. This is a classic Astro-content-collection default, where the layout template for the list page was never given a proper top-level heading and nobody noticed because visually the first post title looks like the headline.
Two: heading-order. Same page, same cause. A list of <h4> elements without an enclosing <h2> or <h3> to step the hierarchy down from. Same fix.
The blog index is the page with zero prose content, built entirely from template code, and it is simultaneously the page with the two structural failures the rest of the site does not have. Meanwhile, the homepage — the one with marketing copy, a hero, three CTAs — scans with zero violations. Structure failures cluster on pages that were generated from a layout file and never had a human re-read them after the scaffold was done.
What we are doing about it
Three changes:
- Darken
.privacy-notetext (#898a8f→#5a5b60), which clears 14 of 15 color-contrast nodes. - Move
<aside class="related-resources">out of<main>in the article layout, which clears all 11landmark-complementary-is-top-levelnodes. - Add an
<h1>to the blog index template and drop the post titles to<h3>, which clears both structural failures on that page.
Projected delta: 27 violations down to 1. The single remaining issue is the Shiki theme comment token on the email marketing post — a third-party syntax-highlighting color we can override locally but would rather fix upstream if the theme maintainer takes the PR.
We are not going to tell you this is already shipped, because it is not. The PR is open at the time of writing and we expect it to land before this article does. If you visit the site after April 15 and still see the 3.13:1 ratio on the newsletter form, the correct reaction is "they missed their own PR deadline" and we will have earned that. By the time you read this the fix should be in, but scanners don't care about our deadlines.
The observation, not a lesson
We have spent a month scanning other people's sites. The first thing we should have done was scan our own. That is not a moral — we knew we should — it is just how it played out. The useful part, for anyone else, is that a scanner on your own templates will find the exact class of bug you find on everyone else's, because shared components are where bugs live. Individual article HTML is almost always fine. Layouts, newsletter blocks, footer widgets, sidebar components — that is the surface area worth scanning.
We will be running axe-core against blog.a11yfix.dev weekly from now on. It should take 15 minutes to notice the next one.
Top comments (0)