We ran our own accessibility audit endpoint on our own site — 60 WCAG violations, fixed in 30 minutes
Two hours ago we shipped POST /v1/audit, an endpoint that runs axe-core against any URL and returns categorised WCAG violations. Then we did the honest thing: pointed it at getsnap.dev.
The result was uncomfortable. The dashboard we built to sell an accessibility audit tool had 60 WCAG 2.0 AA color-contrast violations, most on the landing page you're probably reading this from.
This post walks through exactly what we found, how the API's response made root-causing straightforward, and the 25-file CSS diff that took the score from 60 violations to 0. If you're on a landing page or docs site, you probably have similar issues waiting to be found.
The one-line audit
curl -X POST https://api.getsnap.dev/v1/audit \
-H "X-API-Key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://getsnap.dev","rules":["color-contrast"]}'
In JSON, the response's summary looked like this:
{
"total_violations": 1,
"critical": 0,
"serious": 1,
"moderate": 0,
"minor": 0
}
"One violation" sounds calm. Then we opened the violations[0].nodes array: 60 failing elements under the same rule. axe-core groups by rule, not by DOM node, and I don't blame it.
Each node came with a helpful failure_summary:
Element has insufficient color contrast of 4.46 (foreground color: #ffffff, background color: #6366f1, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1.
4.5:1 is the WCAG 2.0 AA threshold for normal-weight text under 18pt. We were at 4.46:1 — missing by 0.04.
Three patterns, sixty violations
The 60 nodes clustered into three color patterns. If you're on a dark-themed landing page, this list will feel familiar:
| Pattern | Foreground | Background | Ratio | Affected |
|---|---|---|---|---|
| Primary blue on card | #6366f1 | #141414 | 4.11:1 | 28 elements |
| White on primary button | #ffffff | #6366f1 | 4.46:1 | 6 elements |
| Red 'no' in comparison table | #ad3636 | #141414 | 2.94:1 | 26 elements |
Two of those patterns miss by a hair (4.11 and 4.46 vs. required 4.5). One misses by a mile (2.94 vs. 4.5). Every one of them is fixable with a color swap.
Why fixable ≠ trivial
The naive fix — "just make the primary color darker" — doesn't work if the same variable is used both as a text color on dark backgrounds AND as a button background under white text.
getsnap.dev used --primary: #6366f1 for exactly that. Making it darker helps the buttons but hurts the badges. Making it lighter helps the badges but hurts the buttons. There's no single value that satisfies both.
The fix: split the role
The design system term for this is "role tokens." Instead of one --primary, we now have two:
:root {
/* Background usage - buttons, pill badges, borders.
Darkened so white text on it passes 4.5:1 */
--primary: #4f46e5;
/* Text usage - links, inline code, callouts on dark bg.
Lightened so it passes 4.5:1 against var(--bg-card) */
--primary-text: #a5b4fc;
}
Every CSS rule using color: var(--primary) for TEXT was changed to color: var(--primary-text). Rules using it for background or border-color stayed the same. The mechanical part took a Python regex on style.css and one file in docs/:
import re
src = open("style.css").read()
new = re.sub(
r'(?<![-\w])color:\s*var\(--primary\)(?![-\w])',
'color: var(--primary-text)',
src,
)
open("style.css", "w").write(new)
Border and background usages don't match the regex (they're preceded by -color: or background:) so they're untouched.
The comparison table
The red 'no' cells were at 2.94:1 — the worst violations by a wide margin. They came from this rule:
.compare-table-landing .cross {
color: var(--red); /* #ef4444 */
opacity: 0.7;
}
The opacity: 0.7 was doing all the damage. It multiplied the effective color by 0.7, dragging red-400 down into red-800 territory. Fix: drop the opacity, use a lighter red that's already legible:
.compare-table-landing .cross {
color: #fca5a5; /* red-300 - passes 5.1:1 on #141414 */
}
Re-audit
After the palette split, we ran the same curl again:
{
"summary": {
"total_violations": 0,
"critical": 0,
"serious": 0,
"moderate": 0,
"minor": 0
},
"violations": []
}
Zero. And not just color-contrast — the full WCAG 2.0 AA sweep (tags: ["wcag2aa"]) came back clean too.
Cost
The full audit is 1 credit (~$0.001 at Pro rates). We ran it four times during this process: initial audit, verification after step 1, verification after step 2, and a full AA sweep at the end. Total spend: $0.004 in API costs. Elapsed time: about 30 minutes including the audit + fix + re-verify cycles.
If you're wondering whether it's worth building visual regression tests into CI for accessibility, the answer per URL is one-tenth of a cent. Add it to your build.
What this changes about our own SDK / CLI
Nothing on the SDK. But we've updated the CLI docs so getsnap audit is the front-page example for anyone who lands there without a specific rule in mind:
export GETSNAP_KEY=sk_live_...
getsnap audit https://yoursite.com
* https://yoursite.com
Your Site Name
3 violations:
critical 0
serious 1
moderate 2
minor 0
Top violations:
[SERIOUS] color-contrast
Elements must meet minimum color contrast ratio thresholds
60 elements affected https://dequeuniversity.com/rules/axe/4.13/color-contrast
[MODERATE] landmark-one-main
Document should have one main landmark
1 element affected
Every rule that fails gets a deque.university documentation link. That's not us being fancy — that's axe-core's own helpUrl field which we pass through verbatim.
What we're leaving for another day
A few things the audit flagged as "needs review manually" (axe-core's incomplete category) that we're leaving in the follow-up bucket:
- Keyboard navigation on the interactive demo section — needs a focus trap in the tabbed URL picker
- Screen reader labels on the pricing table cells (the green checks and red 'no' need
aria-labels) - Skip-to-content link at the top of every page
None of these are blocking WCAG AA. All three would be easy to knock out in a follow-up round.
Audit your own site
Free tier gives you 100 audits per month. That's plenty for weekly regression runs against every landing page you ship.
Get free API keyTakeaways
- Your dark theme is probably breaking WCAG in ways the design system hides from you.
- Splitting
--primaryinto text-color and background-color roles is the cleanest fix for the button/badge collision. - Opacity multipliers on text almost always break contrast. Use a solid color that already has the effective luminance you want.
- Automated audits catch the mechanical stuff at $0.001 per URL. Keyboard nav, focus order, and screen-reader UX still need human review — but those are 5% of the total violations, not 95%.
- Ship the endpoint first, then run it on your own site. If it embarrasses you, it's working.
Related reading
- A 503 that could not happen — another thing we found by checking our own work
- The accessibility audit endpoint — run the same axe-core checks against your own pages
- Scheduled captures for monitoring — automate the re-check instead of auditing by hand