Most existing forms have a validation experience that visually works and is invisible to users on assistive technology. The fix is usually two ARIA attributes (aria-describedby and aria-invalid) plus a small live region on the error element. The total code change for a typical form is under 50 lines and produces a meaningful improvement in accessibility scores.
This is a step-by-step walkthrough of the wiring, with the specific gotchas that come up when you retrofit it onto an existing form.

Photo by Yusuf Çelik on Pexels
The current state of most forms
A typical existing form has this kind of structure:
<div class="form-field">
<label for="email-input">Email</label>
<input type="email" id="email-input" name="email" />
<div class="error" id="email-error"></div>
</div>
The error div is in the DOM. The validation JavaScript writes a message into it when something is wrong. The CSS styles the error red. From a sighted user's perspective, it works.
From a screen reader user's perspective, the input and the error are unrelated DOM siblings. Focusing the input announces only the label, not the error. The error appears and a user not navigating to it never knows.
The fix is connecting the input to the error programmatically.
Step 1: add aria-describedby
The first attribute. The input gets aria-describedby pointing to the id of the error element.
<input
type="email"
id="email-input"
name="email"
aria-describedby="email-error"
/>
This is what tells the screen reader that when the user focuses the input, the content of the #email-error element is part of the description. The same attribute can take multiple ids (aria-describedby="email-error email-hint") if you also have a static help text element below the input.
The attribute can be on the input from the start; you do not have to add and remove it based on whether an error is present. The screen reader announces the description only when there is content in the referenced element. An empty error div produces no announcement.
Step 2: add aria-invalid dynamically
aria-invalid is the programmatic signal that the input has a validation error. Set it to true when the error shows, false (or remove it) when the error clears.
function showError(input, errorElement, message) {
errorElement.textContent = message;
input.setAttribute('aria-invalid', 'true');
}
function clearError(input, errorElement) {
errorElement.textContent = '';
input.setAttribute('aria-invalid', 'false');
}
aria-invalid is separate from the visual styling. Screen readers announce it as "invalid entry" or similar; sighted users see the red border via CSS. Both signals point at the same state but go to different users.
A common mistake: setting aria-invalid on the input element only, while the visual red border is on a wrapping div. Either move the styling to the input itself (so the visual and the ARIA share a target), or add a corresponding visual class on the input in the same function. Drift between the two is the root cause of most "the form looks valid but the screen reader says invalid" bugs.
Step 3: make the error announce when it appears
The aria-describedby connection is sufficient if the user navigates back to the field after the error appears. It is not sufficient if the error appears while the user is still focused on the field (live re-validation on keystroke after an initial error).
A live region on the error element fixes this:
<div class="error" id="email-error" aria-live="polite" role="status"></div>
aria-live="polite" tells the screen reader to announce changes at the next natural pause, not interrupt the user mid-typing. The role="status" is a redundant signal (many implementations require both for cross-browser support; you can drop role and test with target screen readers if you want minimal markup).
Polite is the right choice for form validation. The assertive variant (aria-live="assertive") interrupts and is reserved for emergencies, which a form error is not.
Step 4: handle the case where the input itself has the busy state
For async validation (username check, address lookup), the user expects feedback that the field is being verified. Without it, the spinner is the only signal, and the screen reader user sees neither.
Two patterns:
Pattern A: Use aria-busy="true" on the input or its wrapper while the check is in flight. Most screen readers will announce that the field is busy.
Pattern B: Use a separate live region for status updates ("Verifying username...") that updates when the check starts and clears when it completes.
Pattern B works more reliably across screen readers, at the cost of slightly more markup. Pattern A is simpler when it works but inconsistent across implementations.
<div class="form-field">
<label for="username-input">Username</label>
<input
type="text"
id="username-input"
name="username"
aria-describedby="username-error username-status"
/>
<div class="error" id="username-error" aria-live="polite"></div>
<div class="status visually-hidden" id="username-status" aria-live="polite"></div>
</div>
The status element is visually hidden (via a standard visually-hidden CSS utility) but exposed to screen readers. It is the audio-only counterpart to the spinner.
Step 5: handle keyboard focus correctly
A field in an error state still needs to be reachable by keyboard. Two patterns that get broken in retrofits:
-
Focus stops working because of overflow rules. Some forms hide error states by clipping with
overflow: hidden, which can also clip the focus outline. Make sure the focus indicator on an error field is still visible. -
Tab order skips the error element. This is usually fine because the error is not interactive (no need to tab into it), but if you have an "edit" or "fix" link in the error message, make sure it is in the tab order with
tabindex="0".
The general rule: keyboard navigation through the form should be unchanged by the presence of errors. The errors are descriptive, not interactive.

Photo by Letícia Alvares on Pexels
Step 6: verify with the standards
Once the wiring is in place, the W3C Web Content Accessibility Guidelines success criteria most relevant to form validation are:
- 3.3.1 Error Identification: Errors must be identified in text. Color alone is not sufficient.
- 3.3.3 Error Suggestion: When the error has known fixes, suggest them. "Add the @ to make this a valid email" is a suggestion; "Invalid email" is not.
- 4.1.3 Status Messages: Status messages (including errors) must be programmatically determined and announced to assistive tech without receiving focus.
The WAI-ARIA Authoring Practices Guide has worked code samples for accessible form validation patterns. If you are unsure about a specific wiring choice, the examples in the APG are the authoritative reference.
Step 7: test with a real screen reader
ARIA wiring that looks correct in code does not always announce correctly. Test with at least one screen reader before merging. NVDA on Windows is free; VoiceOver on macOS is built in. Ten minutes of testing on a real screen reader catches more bugs than any automated audit.
The minimum test pass:
- Tab into the field. Label announces.
- Type an invalid value. Tab away. Error announces.
- Tab back to the field. Label announces with the error as the description.
- Fix the input. Error clears, optionally with an announcement.
- Trigger an async check. Pending status announces. Result announces when ready.
Tools that complement manual testing:
- WAVE browser extension surfaces ARIA wiring gaps inline on the page.
- axe DevTools browser extension catches ARIA misuse and color contrast issues; usable in CI for regression testing.
- WebAIM contrast checker verifies error text and input border colors meet WCAG thresholds.
Step 8: keep the existing styles intact
The whole point of this retrofit is to avoid rebuilding the form. The styles already work for sighted users; the ARIA additions are about adding signal for users on assistive tech.
Specific things to avoid changing:
- The visual layout of the form. The error div stays where it is.
- The color of the error text or the input border. If the colors meet WCAG, leave them.
- The trigger logic (when validation runs). If the timing is wrong, fix it as a separate change, not as part of the accessibility retrofit. Bundling the two makes both harder to review.
The retrofit is a small targeted change. The design improvements come next.
The longer read on the full design
This piece is the wiring mechanics. The full design of inline validation (when to trigger, what to say, async patterns, visual treatment, success states) sits in a longer guide on designing inline form validation that actually helps users on https://137foundry.com. The two pieces stack: this one wires the accessibility, the longer one improves the design that the accessibility now exposes correctly.
The right order is: ARIA retrofit first (cheap, high impact), then the design improvements on top of an accessibility-correct baseline. Skipping the retrofit and only doing the design work means the improved design is still invisible to users on assistive technology, which defeats the point.
Top comments (0)