Docs
Accessibility
How Proa catches common WCAG, WAI-ARIA, and ARIA-in-HTML issues at compile time.
html_sync! and html! check accessibility rules visible in the authored template at compile time. Invalid roles and required attributes can fail compilation; other findings are warnings. Use proa check in CI and browser checks for behavior that static templates cannot prove.
What Proa Checks
Proa checks a subset of rules from three specifications:
- WCAG 2.2, Web Content Accessibility Guidelines (Level A and AA)
- WAI-ARIA 1.2, Accessible Rich Internet Applications
- ARIA in HTML, Rules for which ARIA roles and attributes are valid on which HTML elements
Text Alternatives (WCAG 1.1.1)
Every non-text content element needs a text alternative for screen readers.
// Warning: <img> without alt
html! {
<img src="/photo.jpg" />
}
// ^^^^ missing `alt` attribute — screen readers cannot describe this image.
// Use alt="description" or alt="" for decorative images.
// OK: descriptive alt
html! {
<img src="/photo.jpg" alt="Team photo from the company retreat" />
}
// OK: decorative image (empty alt)
html! {
<img src="/divider.svg" alt="" />
}
This also applies to <input type="image"> (must have alt) and <area> with href (must have alt).
ARIA Role Validation
The role attribute must be one of the 82 roles defined in WAI-ARIA 1.2. Abstract roles (like widget, landmark, structure) cannot be used directly.
// Error: invalid role
html! {
<div role="buttn">"Click"</div>
}
// ^^^^^^ "buttn" is not a valid ARIA role. Did you mean "button"?
// Error: abstract role
html! {
<div role="widget">"Click"</div>
}
// ^^^^^^^ "widget" is an abstract role and cannot be used in content.
// Use a concrete role like "button", "checkbox", or "slider".
Required ARIA Attributes
Some roles require specific ARIA attributes to function correctly with assistive technology:
// Error: role="checkbox" requires aria-checked
html! {
<div role="checkbox">"Accept terms"</div>
}
// ^^^^ role="checkbox" requires the `aria-checked` attribute.
// Add aria-checked="true", aria-checked="false", or aria-checked="mixed".
// OK
html! {
<div role="checkbox" aria_checked="false">"Accept terms"</div>
}
| Role | Required attribute(s) |
|---|---|
checkbox | aria-checked |
combobox | aria-controls, aria-expanded |
heading | aria-level |
menuitemcheckbox | aria-checked |
menuitemradio | aria-checked |
meter | aria-valuenow |
option | aria-selected |
radio | aria-checked |
scrollbar | aria-valuenow |
separator (with tabindex) | aria-valuenow |
slider | aria-valuenow |
spinbutton | aria-valuenow |
switch | aria-checked |
ARIA Attribute Validation
ARIA attribute names must be ones the macro recognizes (the 48 defined in WAI-ARIA 1.2, plus the newer braille and description attributes), and enumerated values must be valid:
// Error: not a real ARIA attribute
html! {
<div aria_labeledby="title">"content"</div>
}
// ^^^^^^^^^^^^^^^ "aria-labeledby" is not a valid ARIA attribute.
// Did you mean "aria-labelledby"?
// Error: invalid value
html! {
<div aria_hidden="yes">"content"</div>
}
// ^^^^ "yes" is not a valid value for aria-hidden.
// Use "true" or "false".
Redundant Roles
HTML elements have implicit ARIA roles. Adding an explicit role that matches is unnecessary noise:
// Warning: redundant role
html! {
<button role="button">"Save"</button>
}
// ^^^^^^^^^^^^^ <button> already has an implicit role of "button".
// Remove the role attribute.
// Warning: prefer native element
html! {
<div role="button" tabindex="0">"Save"</div>
}
// ^^^^^^^^^^^^^ prefer <button> over <div role="button"> —
// native elements provide keyboard support and
// accessibility semantics for free.
Tabindex
Positive tabindex values disrupt the natural tab order and are almost always wrong:
// Warning: positive tabindex
html! {
<input tabindex="5" />
}
// ^^^^^^^^^^^^^ tabindex="5" overrides the natural document tab order.
// Use tabindex="0" to add to natural order, or
// tabindex="-1" for programmatic focus only.
Language
Screen readers use the lang attribute to determine pronunciation rules:
// Warning: <html> without lang
html! {
<html>
<head>
<title>"My Page"</title>
</head>
</html>
}
// ^^^^^ <html> should have a `lang` attribute (e.g., lang="en").
// Screen readers use this to select the correct pronunciation rules.
What Proa Cannot Check
These accessibility requirements depend on runtime behavior, computed CSS, or cross-component context that the html! macro can't see:
| Concern | Why | What to use |
|---|---|---|
| Color contrast ratios | Requires computed CSS styles | axe-core, Lighthouse |
| Keyboard navigation | Requires runtime focus testing | Manual testing, axe-core |
| Focus order | Requires runtime DOM | axe-core |
| Focus visibility | Requires CSS + runtime | Browser DevTools |
| Screen reader announcements | Requires assistive technology | NVDA, VoiceOver |
| Dynamic ARIA state changes | Requires runtime JS | axe-core |
| Cross-component nesting | Fragments composed at different call sites | Nu HTML Checker |
Proa's compile-time checks cover what's visible in the HTML output. For the full picture, pair them with axe-core in your integration tests:
// Example: axe-core in Playwright
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("home page is accessible", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Standards Reference
| Standard | What it covers | Link |
|---|---|---|
| WCAG 2.2 | Success criteria for accessible web content (A, AA, AAA) | w3.org/TR/WCAG22 |
| WAI-ARIA 1.2 | Roles, states, and properties for rich UI widgets | w3.org/TR/wai-aria-1.2 |
| ARIA in HTML | Which ARIA is valid on which HTML elements | w3.org/TR/html-aria |
| HTML-AAM | How HTML maps to platform accessibility APIs | w3.org/TR/html-aam-1.0 |
| Section 508 | US federal accessibility law (references WCAG 2.0 AA) | section508.gov |
| EN 301 549 | EU accessibility standard (references WCAG 2.1 AA) | etsi.org |