Skip to main content

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 -- requires getBoundingClientRect()
  • link-in-text-block -- requires computed styles
  • css-orientation-lock -- requires CSS media query analysis
  • p-as-heading -- requires computed font styling
  • scrollable-region-focusable -- requires overflow computation
  • focus-order-semantics -- requires tab order computation
  • hidden-content -- requires full visibility computation
  • label-content-name-mismatch -- requires rendered visible text
  • frame-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fast_a11y_py-0.2.0.tar.gz (53.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fast_a11y_py-0.2.0-py3-none-any.whl (50.0 kB view details)

Uploaded Python 3

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

Hashes for fast_a11y_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 75d625f47cb9b257208b55e758123cb5ffa72c7553ddbb32cef4c4237e5831d4
MD5 3d9503866e4842180c029138c1a2b624
BLAKE2b-256 32a921abd3258c38bcc6b586ee5dce2b6b35a03344003dfe4a74bceb76cc1eb4

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fast_a11y_py-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 05bd6905ead7b35b535569515a6908b83b93a31edf66787d765de52a8bac8e47
MD5 6ad2eaef8c1776341f1dc7cb52c4b038
BLAKE2b-256 41e3a7ee282dcfce477436f94f9cc3ef9d0a68b8785ff4ce0626faea01deedcb

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page