---
title: Accessibility
description: 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](https://www.w3.org/TR/WCAG22/), Web Content Accessibility Guidelines (Level A and AA)
- [WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/), Accessible Rich Internet Applications
- [ARIA in HTML](https://www.w3.org/TR/html-aria/), 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.

```rust
// 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.

```rust
// 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:

```rust
// 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:

```rust
// 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:

```rust
// 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:

```rust
// 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:

```rust
// 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](https://github.com/dequelabs/axe-core), [Lighthouse](https://developer.chrome.com/docs/lighthouse/accessibility/) |
| 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](https://www.nvaccess.org/), [VoiceOver](https://support.apple.com/guide/voiceover/welcome/mac) |
| Dynamic ARIA state changes | Requires runtime JS | axe-core |
| Cross-component nesting | Fragments composed at different call sites | [Nu HTML Checker](https://validator.w3.org/nu/) |

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:

```typescript
// 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](https://www.w3.org/TR/WCAG22/) |
| WAI-ARIA 1.2 | Roles, states, and properties for rich UI widgets | [w3.org/TR/wai-aria-1.2](https://www.w3.org/TR/wai-aria-1.2/) |
| ARIA in HTML | Which ARIA is valid on which HTML elements | [w3.org/TR/html-aria](https://www.w3.org/TR/html-aria/) |
| HTML-AAM | How HTML maps to platform accessibility APIs | [w3.org/TR/html-aam-1.0](https://www.w3.org/TR/html-aam-1.0/) |
| Section 508 | US federal accessibility law (references WCAG 2.0 AA) | [section508.gov](https://www.section508.gov/) |
| EN 301 549 | EU accessibility standard (references WCAG 2.1 AA) | [etsi.org](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/) |
