A two-second accessibility gate that runs on every push
I already had a vitest harness that renders my Svelte components and mocks the calls into Rust. Adding accessibility checks turned out to be almost free: point axe-core at each rendered screen and assert it finds nothing. So now every push runs axe over the onboarding wizard, the dashboard both empty and full, and the two dialogs, and fails the build if any of them grows a missing label or a broken ARIA attribute. It takes about two seconds.
I scoped it to the actual WCAG A and AA rules on purpose. Axe ships a pile of “best practice” rules on top of the standard, and some of them, like “every bit of content must sit inside a landmark region,” fire constantly when you render a single dialog in isolation instead of a whole page. That’s noise, not a real problem, so I told axe to run only the WCAG-tagged rules. The gate flags things that are actually wrong and stays quiet about things that are only wrong out of context.
Here’s the part I want to be honest about, because it’s easy to oversell this. The tests run in jsdom, which builds a DOM but never lays anything out or paints it. That means the one accessibility check everyone thinks of first, color contrast, cannot run here. Axe tries, fails to get a canvas to measure pixels, and quietly reports the result as “incomplete” rather than pass or fail. So a contrast regression sails straight through this gate. Same story for focus-visible styles. What I’ve actually got is a structural gate: names, roles, labels, headings, form associations. Valuable, and the bulk of what regresses day to day, but not the whole picture.
Rather than pretend otherwise, I wrote the gap into the code comment and filed the other half against my dark-mode issue. Contrast only really starts breaking when you start swapping theme colors, and that’s exactly what dark mode is. When I build it, I’ll add a real-browser axe pass that checks contrast in both themes. Right tool, right moment.
Two small traps on the way in. The matcher library’s TypeScript types were written for an older vitest and simply didn’t attach to the current one, so my type-checker insisted the assertion didn’t exist even though it ran fine. A four-line type declaration of my own fixed it. And before trusting a green result, I fed axe a deliberately broken snippet, an image with no alt text and an empty button, to confirm it actually reports violations. A passing accessibility test that can’t fail is worse than no test, because it tells you you’re covered when you aren’t.
The nice outcome: my existing screens came back clean, so there was nothing to fix, just a gate to keep them that way.
Related reading
Fixing a race, an Esc-key bug, and a duplicate-key crash in Greenhouse
A monotonic counter for out-of-order refreshes, a cancelable Esc-key dialog event, and a duplicate-key crash from a timestamp that collides within a second.
The bug my unit tests could never have found
A kanban board rendered a prop that went stale the moment other data changed, and every mocked test passed because a mock can't express staleness.
Dark mode, and the browser test that fought back
light-dark() made the CSS easy. Three test-tooling failures made the contrast gate hard. Then real screenshots proved the green gate wasn't the whole story.
You might also find useful
Proton Drive
Encrypted cloud storage from the team behind Proton Mail.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreProton Mail
End-to-end encrypted email with zero-access architecture.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreAiralo eSIM
Local data eSIM for travel - no physical SIM swap needed.
This is my Airalo referral link. You get a discount on your first eSIM, and I earn Airalo credit toward mine.
Learn more