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:

PatternForegroundBackgroundRatioAffected
Primary blue on card#6366f1#1414144.11:128 elements
White on primary button#ffffff#6366f14.46:16 elements
Red 'no' in comparison table#ad3636#1414142.94:126 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:

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 key

Takeaways

Related reading