How to Scan a Website for WCAG Accessibility Issues Automatically
Checking one page with a browser extension is easy. Checking the 40 pages of a client site every month, keeping the results and comparing them is not. This guide shows how to run an automated WCAG 2.x scan on many pages at once through an API and get the results back as JSON, with JavaScript, Python or plain curl.
I'll also be clear about the limits up front, because they matter more for accessibility than for almost anything else.
What an automated scan can and can't do
Automated rules are good at things a machine can decide reliably: an image without alternative text, a form field without a label, a button with no accessible name, a missing lang attribute, invalid ARIA, many colour-contrast problems.
They can't judge whether alt text actually describes the image, whether the reading order makes sense, whether a page works with a keyboard and a screen reader, or whether captions are correct. The axe-core maintainers estimate that automated testing finds on average about 57% of WCAG issues. The rest needs manual and assistive-technology testing.
So treat a scan as a fast first pass that helps you find and prioritise issues, not as an answer to "is this site accessible?". A result with zero violations does not mean a site is accessible.
Disclaimer. The tool below runs automated accessibility tests (based on the open-source axe-core® engine) and reports issues it can detect against WCAG 2.x success criteria. The results are not a conformity assessment, certification or proof of compliance with WCAG, EN 301 549, the European Accessibility Act, the German BFSG, the ADA or any other law, and not legal advice. Whether and how accessibility laws apply to your organisation must be assessed by a qualified professional.
axe-core® is a trademark of Deque Systems, Inc. in the US and other countries. The tool is not affiliated with or endorsed by Deque Systems.
A note on WCAG versions and the EAA
If you're looking at this because of the European Accessibility Act (in Germany the BFSG), one detail is worth knowing: the current harmonised standard, EN 301 549 V3.2.1, references WCAG 2.1 AA and has requirements beyond WCAG. A WCAG 2.2 scan is a useful technical check while you prepare, but it doesn't answer whether a site meets those rules.
Also, the default rule set runs the axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa. axe-core has no automated rules for the success criteria WCAG 2.2 added at Level A, so those need manual review too.
The tool: SEO Audit Tool with the accessibility check switched on
I maintain the SEO Audit Tool on Apify, a hosted crawler for technical SEO checks. It has an optional accessibility check: with checkAccessibility: true it opens the audited pages in headless Chrome, runs the unmodified axe-core rules and adds the results to each page record.
What you get per page:
-
violations: rule, impact, WCAG success criteria (such as1.4.3), a short how-to-fix and up to 10 affected elements (CSS selector, failure summary and a snippet of at most 250 characters) -
needsManualReview: checks the engine couldn't decide automatically, listed separately so they don't get lost or overstated -
status:checked,partial(for example a stylesheet failed to load, so the page may look different to visitors) orskipped - a one-line disclaimer in English and German
The summary record lists the most common issues across all pages, and a Markdown report (English or German) is saved as ACCESSIBILITY_REPORT. There's deliberately no accessibility "score" and no pass/fail flag.
It's also polite: robots.txt is respected for every request the browser makes, pages are checked one at a time with at least one second between pages on the same host, and it never logs in or submits forms. Only scan sites you own or are authorised to test.
Run it from the browser
Open the ready-made example Check website accessibility (WCAG), replace the start URL with your site and click Start. Use 1024 MB memory: the browser scan works with 512 MB but takes about twice as long.
Run it with curl
curl -X POST "https://api.apify.com/v2/acts/batuweb~seo-audit-toolkit/run-sync-get-dataset-items?memory=1024" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startUrls": ["https://example.com"],
"maxPages": 10,
"checkBrokenLinks": false,
"checkAccessibility": true,
"accessibilityStandard": "wcag22aa",
"accessibilityLanguage": "en"
}'
You get an array of records: one per audited page (each with an accessibility object), error records for unreachable URLs, and a summary record at the end.
JavaScript (Node.js)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('batuweb/seo-audit-toolkit').call(
{
startUrls: ['https://example.com'],
maxPages: 20,
checkAccessibility: true,
accessibilityMaxPages: 20,
accessibilityStandard: 'wcag21aa', // the WCAG version EN 301 549 V3.2.1 references
},
{ memory: 1024 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const page of items.filter((i) => i.type === 'page')) {
const a = page.accessibility;
if (!a || a.status === 'skipped') continue;
console.log(`${page.url} [${a.status}] violations: ${a.counts.violations}, manual review: ${a.counts.needsManualReview}`);
for (const v of a.violations) console.log(` - ${v.ruleId} (${v.impact}, WCAG ${v.wcagCriteria.join(', ')}): ${v.help} x${v.elementCount}`);
}
Python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("batuweb/seo-audit-toolkit").call(
run_input={
"startUrls": ["https://example.com"],
"maxPages": 20,
"checkAccessibility": True,
"accessibilityIncludeHtmlSnippets": False, # keep page content out of the results
},
memory_mbytes=1024,
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
summary = next(i for i in items if i.get("type") == "summary")["accessibility"]
print("Pages checked:", summary["pagesChecked"], "partial:", summary["pagesPartiallyChecked"])
for rule in summary["topViolations"]:
print(f'{rule["ruleId"]}: {rule["pages"]} page(s), {rule["elements"]} element(s)')
for rule in summary["needsManualReview"]:
print(f'Needs manual review: {rule["ruleId"]} on {rule["pages"]} page(s)')
A real example
I ran it on a small recipe site of mine, cookmushroom.com (10 pages, 1024 MB, about 100 seconds). All 10 pages were fully checked, and there were 0 automated violations. That sounds great, but the useful part was the manual-review list:
-
color-contrast(WCAG 1.4.3) on all 10 pages: the engine couldn't work out the background colour because of a CSS gradient, so a person has to check the contrast. -
link-in-text-block(WCAG 1.4.1) on 5 pages: links inside paragraphs may only be distinguishable by colour.
That's exactly the kind of result to read carefully: "0 violations" here means "nothing the machine could prove wrong", not "accessible".
Tips
-
Pick the rule set on purpose.
wcag21aamatches the WCAG version that EN 301 549 V3.2.1 references;wcag22aaadds the WCAG 2.2 AA rules. -
Look at
partialpages. If a stylesheet or script didn't load, contrast and visibility results can differ from what visitors see. - Schedule it. Run it monthly with an Apify schedule and compare the summaries to catch regressions after redesigns.
-
Turn snippets off (
accessibilityIncludeHtmlSnippets: false) if pages can contain personal data. Only CSS selectors are kept then. - Follow up manually. Use the scan to find the obvious issues fast, then do keyboard, zoom and screen-reader checks on your key templates.
Pricing
You pay the normal SEO audit fee of $0.004 per page (cheaper on paid Apify plans). The accessibility check is free during launch. From 3 November 2026 it costs $0.004 per fully checked page on the Free plan, down to $0.002 on higher plans. Partial and skipped pages are never charged, and you can cap the cost of any run with Maximum cost per run.
Automated testing finds only part of all accessibility barriers. Results help you find and prioritise issues; they are not a conformity assessment, certification or legal advice.
Top comments (0)