A11y Form And Label Validation Rules
Design Log #166 — A11y Form and Label Validation Rules
Written for AI agents. See Log Methodology Note below for details.
Background
@jay-framework/a11y-validator (DL#145, DL#147) already checks that text-like inputs, selects, and textareas have an associated label via <label for>, wrapping <label>, aria-label, or aria-labelledby.
Gaps remain for common form/accessibility mistakes that AI agents and designers make in jay-html:
checkbox/radioare skipped byLABELABLE_INPUTS- empty
aria-label=""and unresolvedaria-labelledbypass as “labeled” - multiple form controls inside one
<label>are accepted - duplicate
idvalues are not flagged - orphan
<label for="...">pointing at a missingidis not flagged
Problem
Static validation under-reports WCAG 1.3.1 / 4.1.2 form naming issues. Agents self-correct only when findings include actionable suggestion text.
Questions and Answers
Q1: One PR or several?
A: One PR for the whole form/label scope. Nesting of interactive elements, lang, and runtime axe stay out of scope.
Q2: Severity?
A:
| Finding | Severity |
|---|---|
| Missing label (incl. checkbox/radio) | error |
Empty aria-label |
error |
aria-labelledby with missing/empty id refs |
error |
Duplicate id |
error |
Multiple labelable controls in one <label> |
warning |
Orphan label[for] (no matching id) |
warning |
Q3: Does wrapping <label> still count for checkbox/radio?
A: Yes — same association rules as other inputs. Explicit for/id preferred when multiple controls share a visual group; fieldset/legend is not required in this PR.
Q4: How does aria-labelledby resolve?
A: Split on whitespace; every token must match an element id in the same jay-html file. Missing any token → error. Empty attribute → error.
Q5: Version bump in the PR?
A: No. Maintainers bump @jay-framework/a11y-validator on release (currently 0.22.2).
Design
Rules (additions / tightenings)
Extend labelable inputs — add
checkbox,radiotoLABELABLE_INPUTS. Still skiphidden,submit,button,reset.Empty
aria-label— if attribute is present andtrim()is empty → error (do not treat as labeled).Broken
aria-labelledby— if present: empty / only whitespace → error; any id token not found in the document → error. If all ids resolve, treat as labeled (skip “no label” finding).Multiple controls in one
<label>— count labelable descendants (inputexcept ignored types,select,textarea). If count > 1 → warning on<label>.Duplicate
id— collect allidattributes; any value used more than once → error per duplicate occurrence after the first (or one finding listing the id).Orphan
label[for]— ifforis non-empty and no element has thatid→ warning.
Implementation approach
Single pre-pass over the DOM:
Set/Mapof allid→ countSetof existing ids for ARIA/forlookup- for each
<label>, count labelable descendants and checkfor
Then existing walkElements + tightened checkLabel.
Examples
✅ Good:
<label for="email">Email</label>
<input type="email" id="email" />
<label><input type="checkbox" /> Agree</label>
<input type="radio" id="a" aria-labelledby="opt-a" />
<span id="opt-a">Option A</span>
❌ Bad:
<input type="checkbox" />
<input aria-label="" />
<input aria-labelledby="missing" />
<label>From <input type="date" /> To <input type="date" /></label>
<div id="x"></div>
<span id="x"></span>
<label for="nope">Name</label>
Implementation Plan
Phase 1: Design log + index
- This document +
design-log/index.mdentry under validation/plugins.
Phase 2: Tests
- Vitest cases for each rule (pass + fail). No
toContainon code files.
Phase 3: Implementation
- Update
a11y-validator.tshelpers andvalidate. - Run package tests.
Phase 4: Catalog + results
- Append new rows to DL#147 a11y table.
- Append Implementation Results here.
Verification Criteria
- checkbox/radio without association → error
aria-label=""→ error; non-empty → passes label checkaria-labelledbymissing target → error- two inputs in one label → warning
- duplicate ids → error
label forwithout matching id → warning- Existing label/
alt/button/tabindex tests still pass
Trade-offs
| Decision | Benefit | Cost |
|---|---|---|
| Per-file id uniqueness only | Matches jay-html validation model | Won't catch cross-file collisions |
| No fieldset/legend rule yet | Keeps PR focused | Radio groups still weak without legend |
| Warning for multi-control label | Avoids hard break on legacy templates | Agents may ignore warnings |
Out of Scope
- Nested interactive (
<a>in<a>) <html lang>- Runtime axe / focus / toast timing
- Color-only / contrast (design-system-validator)
Implementation Results
Phase 2–3: Tests + code
- Extended
LABELABLE_INPUTSwithcheckbox/radio - Tightened
checkLabelfor emptyaria-labeland resolvedaria-labelledbytokens against file ids - Pre-pass: duplicate
idcounts, orphanlabel[for], multi-control<label> - Tests added in
a11y-validator.test.ts
Test results
packages/plugins/a11y-validator: 54/54 passing
Catalog
Updated DL#147 a11y rules table with the new rows.
Deviations from design
None material. Multi-control warning message wording uses “contains N form controls” (same intent as designed).
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.