Overview
Accessibility in Chirp is a contract, not a convention.app.check() scans your templates at startup and in CI, and fails loud on five common regressions: htmx handlers on non-interactive elements, unlabeled form fields, images missing alt, skipped heading levels, and layouts with no <main>landmark.
This page shows what each check catches, how to promote it to a hard build failure, and the semantic-HTML and ARIA patterns Chirp expects you to ship. If you arrived here from a contract warning, the accessibility checks below name your fix.
Accessibility contract checks
Five checks run wherever [[docs/quality/contracts-debugging/_index|app.check() validates contracts]] — at startup in debug mode, and in CI via chirp check myapp:app. The contract message names the offending template and the concrete fix. All five emit at WARNINGseverity, so they surface regressions without blocking an app mid-migration.
| Category | Catches | Fix |
|---|---|---|
a11y_interactive |
htmx URL attributes (hx-get/hx-post/hx-put/hx-patch/hx-delete) on non-interactive elements. |
Use<button> or <a>, or add role="button" tabindex="0". |
a11y_label |
<input>/<select>/<textarea> with no associated label — no matching <label for="…">, no wrapping <label>, no aria-label/aria-labelledby. Hidden, submit, button, and reset inputs are exempt. |
Add<label for="id">, wrap the field in a <label>, or set aria-label. |
a11y_alt |
<img> tags with no altattribute. |
Usealt="…" for meaningful images, alt=""for decorative ones. |
a11y_heading |
Heading levels that skip — for example<h1> straight to <h3> with no <h2>— which breaks the document outline for screen readers. |
Use heading levels in order with no gaps. |
a11y_landmark |
Layout templates with no<main> (or role="main") landmark. Only layouts are checked; pages inherit landmark structure from the layout. |
Add<main> or role="main"to the layout. |
Semantic HTML
The checks reward semantic markup: elements that convey meaning give screen readers a structure to navigate, and they keepa11y_heading and a11y_landmarkquiet for free.
header,main,nav,footerfor page structurearticle,sectionfor content groupingh1–h6for headings, in order, no skipsbuttonfor actions,afor navigationlabelfor form controls
<header>
<nav aria-label="Main navigation">...</nav>
</header>
<main>
<article aria-label="Question and answer">
<h2>Question</h2>
<p>...</p>
</article>
</main>
ARIA for dynamic content
When content updates via htmx or SSE, ARIA announces the change to assistive technology:
aria-live="polite"— announces updates without interruptingaria-atomic="true"— reads the entire region when it changesaria-label— describes regions and controls
The RAG demo uses this pattern for the streaming answer region:
<div sse-swap="answer" hx-target="this" aria-live="polite" aria-atomic="true">
<span class="chirpui-text-muted">Searching docs and generating answer…</span>
{{ typing_indicator() }}
</div>
Source: examples/chirpui/rag_demo/templates/ask.html.
Forms
- Associate
labelwith inputs viafor/id, or wrap the input - Use
aria-describedbyfor validation messages - Use
aria-invalidwhen a field has errors - Provide
aria-labelfor icon-only buttons
<label for="question-input">Your question</label>
<textarea id="question-input" name="question" aria-describedby="validation"></textarea>
<div id="validation" role="alert" aria-live="polite"></div>
Images and keyboard
- Always provide
altfor images — empty stringalt=""for decorative ones - Use
aria-labelortitlefor icon-only buttons - Keep every interactive element focusable and operable by keyboard
- Use visible focus styles; never set
outline: nonewithout a replacement - For custom controls (such as switches), use
role="switch"witharia-checked