666 Accessibility Errors, Zero Excuses
A pa11y-ci audit of my portfolio found 666 errors — built by someone who ships accessible component libraries. The triage that got it to zero axe violations, and the two errors I can't fix.
The number was almost funny: 666 errors from a pa11y-ci run across my portfolio. A portfolio whose featured case study is about building ARIA 1.2-compliant component libraries. A portfolio written by someone who tests with real screen readers.
I'm publishing the number because the gap it exposes is the most common accessibility failure I know, and it has nothing to do with not caring. The libraries were clean because they were built under test — every combo and datepicker release runs against the spec. The site drifted because it wasn't. That's the accessibility tax in its purest form: you don't pay it where you're rigorous, you pay it everywhere you assumed rigor would carry over.
666 Is a Smaller Number Than It Looks
The first thing to understand about a three-digit error count: it's almost never hundreds of problems. It's a handful of problems rendered hundreds of times.
A card component with one low-contrast badge appears on thirteen case-study pages, each listing three more cards. A star-rating component missing a role shows up in every review section. One bad token in the dark theme touches every button on every page. The error count multiplies by usage; the cause count doesn't.
So the first move wasn't fixing anything — it was clustering. Sort the report by selector, collapse repeated components, and 666 errors resolved into a short list of root causes:
- Contrast failures coming from tokens — dark-mode buttons, syntax-highlighting colors inside embedded code sandboxes, the star-rating component
- ARIA that was wrong, not missing — attributes that looked diligent and failed validation
- A theming bug that only appeared under one condition — more on this one, it's the interesting one
- Third-party internals I don't control
Fix Tokens First, Because They Cascade
The contrast cluster went first, and not by touching components. This site runs on a layered token architecture — components only read semantic tokens like --t-color-fg-muted, never raw values. Which means a contrast failure isn't a component bug; it's a token value bug, and fixing it in one place fixes every component that reads it.
That's the unglamorous payoff of token discipline. "Fix dark-mode button contrast" wasn't a sweep through button variants — it was adjusting values in the dark theme's alias file and watching a few dozen errors disappear on the next audit run. The star-rating and sandbox-syntax fixes worked the same way. One decision, many corrections.
If your error report is full of contrast failures and your fix list is full of component names, that's the audit telling you that you don't have a token system — you have a naming convention.
The ARIA That Fails Is the ARIA That Looks Careful
Two findings in the ARIA cluster are worth naming because both came from trying:
aria-haspopup="dialog" — removed. It reads like conscientious markup. But axe flagged it, and the fix was deletion, not addition. This is the pattern worth internalizing: incorrect ARIA is worse than absent ARIA, because native HTML has correct semantics by default and every attribute you add is a claim the browser will faithfully repeat to assistive tech — true or not.
The Rating component — role="img" added. A row of stars is an image of a rating, semantically. Without the role, a screen reader walks the individual glyphs; with it (plus a label), it announces the meaning. One attribute, but it changes the component from decoration into information.
The Bug That Only Existed Sometimes
My favorite finding: pages passed or failed depending on which theme the auditor happened to land on.
The culprit was hydration. This site sets data-theme on <html> in a pre-paint script — before React loads — so there's no flash of the wrong theme. But after hydration, React reconciled the attribute away under certain conditions, silently dropping the theme. Visually you might not notice for a second; the audit noticed immediately, because contrast math changes with the theme.
Two fixes: the theme hook now re-asserts data-theme after hydration, and the generated CSS gained a prefers-color-scheme fallback so that even in the no-JS, no-attribute worst case, the page respects the OS preference with passing contrast.
The lesson generalizes: audit in every theme, not the default one. A multi-theme site has a contrast surface per theme, and the failures hide in the ones you don't develop in.
The Two Errors I Can't Fix
Final score: 666 → 2. Zero axe violations across every page. The remaining two live inside a third-party code-sandbox embed on the playground page — an internal scroll container with tabindex="-1" and a hidden form the widget uses to talk to its service. Fixing them means forking the dependency.
I'm listing them instead of hiding them because a claim of "100% accessible" is usually a claim of "we stopped measuring." The honest state of this site is: everything I control passes WCAG 2.1 AA with zero axe violations, and two known issues exist in a dependency, documented and monitored. If the vendor fixes their internals, my next audit run will tell me.
The Part That Actually Matters: The Harness Stays
The audit isn't the achievement. The standing harness is: pa11y-ci with both the axe and HTML CodeSniffer runners against WCAG 2.1 AA, every key page in the URL list, wired into the workflow so drift gets caught as errors, not discovered as embarrassment.
The proof came recently. A new page shipped on this site — a token-browser page with big data tables, scrollable regions, decorative glyphs, the works. First audit run flagged real issues: a low-contrast "inherits" dash repeated ~45 times (one token bug, again), and an aria-label on an element whose role doesn't permit one. Both fixed within the hour, page shipped at zero errors — because the harness existed before the page did.
That's the whole argument. Accessibility as an audit is a cleanup you'll repeat forever. Accessibility as infrastructure is a tax you pay once, in CI, at the moment each mistake is cheapest.
The component-level version of this discipline — live regions, keyboard matrices, screen-reader testing — is in the ReactZero case study. The token architecture that made the contrast fixes cheap is documented, with the site itself as the artifact, at /design-system.