The slow way to audit is opening pages one at a time in a browser extension. The fast way is to run the engine over a URL list headlessly and sort the output by rule, because the same three defects usually repeat across every template. Two commands do most of the work:
# one page, machine-readable, exit code reflects violations
npx @axe-core/cli https://example.com --exit --save axe.json
# many pages from a sitemap, in parallel
npx pa11y-ci --sitemap https://example.com/sitemap.xml \
--sitemap-exclude '/tag/' --threshold 0
Then group the results before reading them. A hundred violations that collapse into four rules is a morning's work; a hundred read one by one is a week.
jq -r '.[0].violations[] | .id + "\t" + (.nodes|length|tostring)' axe.json | sort -k2 -rn
On any component-based front end, a violation reported on 340 pages is one file. Before touching anything, map each violation's CSS selector back to the component that renders it and count distinct components rather than distinct nodes. A card grid with an unlabelled icon button is a single edit that clears hundreds of findings, while a genuinely one-off contrast failure in a marketing hero is one edit that clears one. Sorting by that ratio is the highest-leverage ten minutes in the whole process.
For a quick pass on a page you are already looking at, paste these into DevTools. Each answers a question faster than launching a scanner:
// images with no alt attribute at all
$$('img:not([alt])')
// form controls with no accessible name
$$('input,select,textarea').filter(el =>
!el.labels?.length && !el.getAttribute('aria-label') &&
!el.getAttribute('aria-labelledby') && el.type !== 'hidden')
// heading order, to spot skipped levels
$$('h1,h2,h3,h4,h5,h6').map(h => h.tagName + ' ' + h.textContent.trim().slice(0,60))
// vague link text
$$('a').filter(a => /^(click here|read more|learn more|here|more)$/i
.test(a.textContent.trim()))
// focus styles someone removed
$$('*').filter(el => getComputedStyle(el).outlineStyle === 'none' && el.tabIndex >= 0)
These are not a substitute for axe. They are a triage pass that takes twenty seconds and tells you whether the page is worth a full audit yet.
Retro-fixing accessibility is expensive because the defects accumulate between audits. Gating stops the accumulation, and the cheapest gate is a component-level assertion inside the test suite you already run:
import { axe } from 'jest-axe';
test('checkout form has no axe violations', async () => {
const { container } = render(<CheckoutForm />);
expect(await axe(container)).toHaveNoViolations();
});
Component tests run in milliseconds and pin the fix in place. Reserve pa11y-ci against a deployed preview for the handful of templates where composition matters — landmark structure, skip links, page title uniqueness. Set the threshold to the current violation count rather than zero on an existing site, then ratchet it down; a gate that fails on day one gets disabled on day two.
Automated rules reliably catch roughly a third of WCAG failures, and the third they miss contains most of the ones that actually block someone. The fastest manual sweep is keyboard-only, and it takes about five minutes per template:
outline: none with no replacement.div with an onclick handler. Make it a button.A handful of shortcuts remove most of the remaining setup cost. Chrome DevTools' colour picker shows a live contrast ratio with AA and AAA ticks the moment you click a text colour swatch, which is faster than pasting hex values into a checker. The Elements panel's Accessibility pane gives you the computed accessible name for the selected node, settling "is this button labelled" instantly. DevTools' Rendering panel emulates prefers-reduced-motion: reduce and forced colours without touching OS settings. And on macOS, VoiceOver toggles with Cmd+F5 — even thirty seconds of Ctrl+Option+Right through a card grid exposes duplicated link text that reads as "read more, read more, read more" and never shows up in a scan. Beginners wanting the longer version can start with the beginner walkthrough.