Fast, zero-DOM accessibility checker with axe-core compatible output
Project description
fast-a11y
Fast, zero-DOM accessibility checker with axe-core compatible output. Runs on raw HTML using static analysis -- no browser, no Selenium, no Playwright.
Why?
axe-core is the gold standard for accessibility testing, but it requires a full DOM environment (JSDOM or a real browser). For crawlers, CI pipelines, and build tools processing thousands of pages, that's a memory and performance bottleneck.
fast-a11y implements 86 WCAG rules using only Python's stdlib html.parser. It returns the exact same output format as axe-core, so it's a drop-in replacement.
| axe-core + browser | fast-a11y | |
|---|---|---|
| 1000 elements | ~200-500MB, ~2-5s | ~5MB, ~30ms |
| Requires browser/DOM | Yes | No |
| Output format | AxeResults | AxeResults (identical) |
| WCAG rules | ~95 | 86 |
| Dependencies | Heavy | Zero (stdlib only) |
Install
pip install fast-a11y
Usage
from fast_a11y import fast_a11y
html = """<!DOCTYPE html>
<html lang="en">
<head><title>My Page</title></head>
<body>
<img src="photo.jpg">
<a href="/page"></a>
</body>
</html>"""
results = fast_a11y(html)
print(results["violations"])
# [
# {"id": "image-alt", "impact": "critical", "nodes": [...]},
# {"id": "link-name", "impact": "serious", "nodes": [...]},
# ]
Options
from fast_a11y import fast_a11y
# Filter by WCAG tags (same as axe-core)
results = fast_a11y(html, {"runOnly": {"type": "tag", "values": ["wcag2a", "wcag2aa"]}})
# Filter by specific rules
results = fast_a11y(html, {"runOnly": {"type": "rule", "values": ["image-alt", "link-name"]}})
# Disable specific rules
results = fast_a11y(html, {"rules": {"color-contrast": {"enabled": False}}})
# Include URL in output
results = fast_a11y(html, url="https://example.com/page")
# Pre-fetched external stylesheets for improved color contrast analysis
results = fast_a11y(html, external_stylesheets=[css_string1, css_string2])
Color Contrast
The color-contrast rule does full static analysis including CSS variable resolution and WCAG level grading.
External stylesheets
fast-a11y stays zero-network. Fetch <link rel="stylesheet"> URLs yourself and pass the CSS strings in:
import urllib.request
from fast_a11y import fast_a11y
with urllib.request.urlopen("https://example.com/styles.css") as r:
sheet = r.read().decode()
results = fast_a11y(html, external_stylesheets=[sheet])
CSS variable resolution
Colors and font sizes defined as CSS custom properties are fully resolved — including chained variables and fallbacks. Works with Tailwind v4, Bootstrap 5, WordPress presets, and any design token system:
/* In your stylesheet */
:root {
--color-grey-900: #111827;
--color-text-primary: var(--color-grey-900); /* chained */
}
p { color: var(--color-text-primary); background-color: #fff; }
# fast-a11y resolves --color-text-primary → --color-grey-900 → #111827
results = fast_a11y(html, external_stylesheets=[css])
# → passes, ratio 16.1:1
WCAG level grading
Every resolved contrast check reports its WCAG level in data["wcagLevel"]:
| Level | Normal text | Large text (≥18pt or ≥14pt bold) |
|---|---|---|
"AAA" |
≥ 7:1 | ≥ 4.5:1 |
"AA" |
≥ 4.5:1 | ≥ 3:1 |
"fail" |
< 4.5:1 | < 3:1 |
violation = next((v for v in results["violations"] if v["id"] == "color-contrast"), None)
node = violation["nodes"][0] if violation else None
print(node["any"][0]["data"])
# {
# "fgColor": "rgb(170, 170, 170)",
# "bgColor": "rgb(255, 255, 255)",
# "contrastRatio": "2.32",
# "wcagLevel": "fail",
# "requiredRatio": 4.5,
# }
Colors that can't be resolved statically (background images, truly unknown variables) are reported as incomplete rather than violations.
Output Format
The output is identical to axe-core's AxeResults:
{
"testEngine": {"name": "fast-a11y", "version": "0.2.0"},
"testRunner": {"name": "fast-a11y"},
"testEnvironment": {"userAgent": "", "windowWidth": 0, "windowHeight": 0},
"url": "",
"timestamp": "2026-01-01T00:00:00+00:00",
"toolOptions": {},
"passes": [...],
"violations": [...],
"incomplete": [...],
"inapplicable": [...],
}
Each RuleResult contains id, impact, tags, description, help, helpUrl, and nodes[] -- exactly matching axe-core.
Rules Covered (86)
Text Alternatives
image-alt, input-image-alt, object-alt, role-img-alt, svg-img-alt, area-alt, server-side-image-map
Language
html-has-lang, html-lang-valid, html-xml-lang-mismatch, valid-lang
Structure
document-title, definition-list, dlitem, list, listitem, heading-order, empty-heading, empty-table-header, duplicate-id, duplicate-id-aria, nested-interactive, page-has-heading-one
Forms
label, select-name, input-button-name, button-name, form-field-multiple-labels, autocomplete-valid, label-title-only
ARIA (25 rules)
aria-allowed-attr, aria-allowed-role, aria-hidden-body, aria-hidden-focus, aria-required-attr, aria-required-children, aria-required-parent, aria-roles, aria-valid-attr, aria-valid-attr-value, aria-roledescription, aria-input-field-name, aria-toggle-field-name, aria-command-name, aria-meter-name, aria-progressbar-name, aria-tooltip-name, aria-treeitem-name, aria-dialog-name, aria-text, aria-deprecated-role, aria-prohibited-attr, aria-braille-equivalent, aria-conditional-attr, presentation-role-conflict
Navigation
link-name, frame-title, frame-title-unique, bypass, tabindex, accesskeys, region
Media & Time
blink, marquee, meta-refresh, meta-refresh-no-exceptions, meta-viewport, meta-viewport-large, no-autoplay-audio, video-caption
Tables
td-headers-attr, th-has-data-cells, td-has-header, table-duplicate-name, table-fake-caption, scope-attr-valid
Landmarks
landmark-one-main, landmark-no-duplicate-main, landmark-no-duplicate-banner, landmark-no-duplicate-contentinfo, landmark-banner-is-top-level, landmark-contentinfo-is-top-level, landmark-complementary-is-top-level, landmark-main-is-top-level, landmark-unique
Color Contrast
color-contrast -- Full static analysis with CSS variable resolution, external stylesheet support, and WCAG AA/AAA grading. See Color Contrast above.
Rules NOT Covered (~9)
These rules fundamentally require a rendered DOM:
target-size-- requiresgetBoundingClientRect()link-in-text-block-- requires computed stylescss-orientation-lock-- requires CSS media query analysisp-as-heading-- requires computed font stylingscrollable-region-focusable-- requires overflow computationfocus-order-semantics-- requires tab order computationhidden-content-- requires full visibility computationlabel-content-name-mismatch-- requires rendered visible textframe-tested-- runtime axe concept
Replacing axe-core
If you're currently using axe-core with a browser:
# Before (axe-core + Playwright)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content(html)
results = page.evaluate("axe.run()")
# After (fast-a11y)
from fast_a11y import fast_a11y
results = fast_a11y(html)
Same output format. Synchronous. No browser. 100x less memory.
See Also
| Package | Description |
|---|---|
| @probeo/fast-a11y | TypeScript version of this package |
| workflow-py | Stage-based pipeline engine -- use fast-a11y as a step |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fast_a11y_py-0.2.0.tar.gz.
File metadata
- Download URL: fast_a11y_py-0.2.0.tar.gz
- Upload date:
- Size: 53.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75d625f47cb9b257208b55e758123cb5ffa72c7553ddbb32cef4c4237e5831d4
|
|
| MD5 |
3d9503866e4842180c029138c1a2b624
|
|
| BLAKE2b-256 |
32a921abd3258c38bcc6b586ee5dce2b6b35a03344003dfe4a74bceb76cc1eb4
|
Provenance
The following attestation bundles were made for fast_a11y_py-0.2.0.tar.gz:
Publisher:
publish.yml on probeo-io/fast-a11y-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fast_a11y_py-0.2.0.tar.gz -
Subject digest:
75d625f47cb9b257208b55e758123cb5ffa72c7553ddbb32cef4c4237e5831d4 - Sigstore transparency entry: 1699179262
- Sigstore integration time:
-
Permalink:
probeo-io/fast-a11y-py@767433978de38adc04e57928e3749bb37ff04d5e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/probeo-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@767433978de38adc04e57928e3749bb37ff04d5e -
Trigger Event:
release
-
Statement type:
File details
Details for the file fast_a11y_py-0.2.0-py3-none-any.whl.
File metadata
- Download URL: fast_a11y_py-0.2.0-py3-none-any.whl
- Upload date:
- Size: 50.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05bd6905ead7b35b535569515a6908b83b93a31edf66787d765de52a8bac8e47
|
|
| MD5 |
6ad2eaef8c1776341f1dc7cb52c4b038
|
|
| BLAKE2b-256 |
41e3a7ee282dcfce477436f94f9cc3ef9d0a68b8785ff4ce0626faea01deedcb
|
Provenance
The following attestation bundles were made for fast_a11y_py-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on probeo-io/fast-a11y-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fast_a11y_py-0.2.0-py3-none-any.whl -
Subject digest:
05bd6905ead7b35b535569515a6908b83b93a31edf66787d765de52a8bac8e47 - Sigstore transparency entry: 1699179325
- Sigstore integration time:
-
Permalink:
probeo-io/fast-a11y-py@767433978de38adc04e57928e3749bb37ff04d5e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/probeo-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@767433978de38adc04e57928e3749bb37ff04d5e -
Trigger Event:
release
-
Statement type: