Kida can statically analyze parsed templates for common accessibility issues — missing alt text, skipped heading levels, missinglangattributes, and unlabeled form elements. Checks run against the template AST, so they catch issues in static markup without requiring a render pass.
from kida.analysis.a11y import check_a11y
Available Rules
| Rule | Severity | What It Checks |
|---|---|---|
img-alt |
error | <img> tags must have an alt attribute. Images with role="presentation" or role="none"are exempt. |
heading-order |
warning | Heading levels must not skip (e.g.,<h2> directly followed by <h4>). |
html-lang |
warning | The<html> tag must have a langattribute. |
input-label |
warning | <input>, <select>, and <textarea> elements must have an associated <label for="...">, aria-label, aria-labelledby, or titleattribute. Hidden inputs are exempt. |
Usage
Python API
Run accessibility checks on any parsed template AST:
from kida import Environment, DictLoader
from kida.analysis.a11y import check_a11y
env = Environment(
loader=DictLoader({
"page.html": """
<html>
<body>
<img src="photo.jpg">
<h1>Title</h1>
<h3>Skipped h2</h3>
<input type="text" name="email">
</body>
</html>
"""
}),
preserve_ast=True,
)
template = env.get_template("page.html")
issues = check_a11y(template._optimized_ast)
for issue in issues:
print(f"Line {issue.lineno}: [{issue.rule}] {issue.message}")
Output:
Line 2: [html-lang] <html> missing lang attribute
Line 4: [img-alt] <img> missing alt attribute
Line 6: [heading-order] Heading level skipped: <h3> after <h1> (expected <h2>)
Line 7: [input-label] Form element missing associated <label> or aria-label
CLI
kida check --a11y templates/
A11yIssue
Each finding is returned as anA11yIssuedataclass:
| Field | Type | Description |
|---|---|---|
lineno |
int |
Line number in the template source |
col_offset |
int |
Column offset |
rule |
str |
Rule identifier (img-alt, heading-order, html-lang, input-label) |
message |
str |
Human-readable description of the issue |
severity |
str |
"error" or "warning" (default: "warning") |
Results are sorted by(lineno, col_offset).
Build System Integration
Integrate accessibility linting into a build pipeline to fail on errors:
from kida import Environment, FileSystemLoader
from kida.analysis.a11y import check_a11y
env = Environment(
loader=FileSystemLoader("templates/"),
preserve_ast=True,
)
errors = []
for template_name in env.loader.list_templates():
template = env.get_template(template_name)
issues = check_a11y(template._optimized_ast)
for issue in issues:
errors.append((template_name, issue))
if errors:
for name, issue in errors:
print(f"{name}:{issue.lineno} [{issue.rule}] {issue.message}")
raise SystemExit(1)
Examples of Violations and Fixes
img-alt
Missingalt attribute on an <img>tag.
<!-- violation -->
<img src="hero.jpg">
<!-- fix: add descriptive alt text -->
<img src="hero.jpg" alt="Mountain landscape at sunset">
<!-- fix: decorative image — mark as presentational -->
<img src="divider.png" role="presentation">
heading-order
Heading levels must increase by one. Skipping from<h1> to <h3>is a violation.
<!-- violation -->
<h1>Page Title</h1>
<h3>Subsection</h3>
<!-- fix -->
<h1>Page Title</h1>
<h2>Subsection</h2>
html-lang
The<html>element must declare a language.
<!-- violation -->
<html>
<!-- fix -->
<html lang="en">
input-label
Form elements need an associated label. The linter accepts any of: a<label> with a matching for attribute, aria-label, aria-labelledby, or title.
<!-- violation -->
<input type="text" name="email">
<!-- fix: explicit label -->
<label for="email">Email</label>
<input type="text" name="email" id="email">
<!-- fix: aria-label -->
<input type="text" name="email" aria-label="Email address">
<!-- exempt: hidden inputs are skipped -->
<input type="hidden" name="csrf_token" value="abc">
Configuration
Thecheck_a11y()function runs all rules unconditionally. To filter results by rule or severity, post-process the returned list:
issues = check_a11y(template._optimized_ast)
# Only errors
errors = [i for i in issues if i.severity == "error"]
# Exclude specific rules
filtered = [i for i in issues if i.rule not in {"heading-order"}]
# Group by rule
from itertools import groupby
by_rule = {k: list(v) for k, v in groupby(issues, key=lambda i: i.rule)}
To enforce a strict policy in CI, treat all issues as failures:
issues = check_a11y(template._optimized_ast)
if issues:
for issue in issues:
print(f" Line {issue.lineno}: [{issue.rule}] {issue.message}")
raise SystemExit(1)
To enforce only errors (ignoring warnings):
errors = [i for i in check_a11y(template._optimized_ast) if i.severity == "error"]
if errors:
raise SystemExit(1)
See Also
- Static Analysis — Dependency and purity analysis
- Template Coverage — Line-level coverage tracking