June 10, 2024 · reala11y team
Accessible form validation errors: aria-invalid and focus
Make WordPress form errors usable: associate messages with fields via aria-describedby, set aria-invalid, manage focus, and announce inline validation. WCAG 3.3.1 and 3.3.3.
A label tells a user what to type. An error message tells them why what they typed was rejected — and that second job is where most WordPress forms quietly fall apart. The field turns red, focus stays put, and a screen-reader user, who never saw the red, submits the same broken form again. They have no idea anything went wrong.
Two success criteria govern this. 3.3.1 Error Identification (Level A) requires that when input is rejected, the error is described to the user in text. 3.3.3 Error Suggestion (Level AA) goes further: when you know how to fix it, suggest the fix. This guide is the mechanics of doing both accessibly. It assumes you’ve already got visible, associated labels — if not, start with our accessible forms guide.
Identify the error in text, not just colour
The baseline failure is signalling an error by appearance alone:
<!-- Inaccessible: the only signal is a red border -->
<input type="email" class="has-error">
A red border communicates nothing to a screen reader and nothing to a colour-blind user who can’t distinguish it. WCAG 3.3.1 asks for a text message that names the problem. WCAG 3.3.3 asks you to make it actionable where you can:
- Weak: “Invalid input.”
- Better (3.3.1): “Email address is not valid.”
- Best (3.3.3): “Email address is not valid. Use a format like [email protected].”
The suggestion is what turns a dead end into a path forward.
Wire the message to the field
A message floating near a field isn’t enough — assistive technology needs the association in the markup. Use aria-describedby to point the input at its error text, and set aria-invalid="true" so the field announces its failed state:
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
>
<p id="email-error">Email address is not valid. Use a format like [email protected].</p>
Now a screen reader announces the label, the “invalid” state, and the error text together. Two things to keep honest:
- Set
aria-invalid="true"only when the field is actually invalid, and remove it (or set it to"false") once the user corrects the entry. A field that permanently claims to be invalid is noise. - If a field has both a hint and an error,
aria-describedbycan reference both IDs:aria-describedby="email-hint email-error".
Announce errors that appear dynamically
Inline (client-side) validation introduces a new problem: the error appears after the page has loaded, so a screen reader won’t read it unless you tell it to. Put error text inside a live region so it’s announced when it changes.
For a single field validated on blur, role="alert" on the message container works well — it’s an assertive live region, so it interrupts and reads immediately:
<p id="email-error" role="alert">
Email address is not valid. Use a format like [email protected].
</p>
For a summary that appears on submit, a polite live region is usually calmer:
<div role="status" aria-live="polite" id="form-status"></div>
A reliable rule of thumb: use role="alert" for an urgent, single-error correction, and aria-live="polite" for a status summary the user isn’t actively waiting on. Don’t wrap the entire form in a live region — you’ll flood the user with announcements on every keystroke.
Manage focus on submit
When a form fails validation, leaving focus wherever the submit button was strands keyboard and screen-reader users. Two patterns work:
- Focus the first invalid field. Quick and predictable for short forms.
- Focus an error summary at the top. Better for longer forms: a heading like “There are 3 problems with your submission” followed by a list of links, each jumping to the field it describes.
// On a failed submit, move focus to the summary so it's read out.
const summary = document.getElementById('error-summary');
summary.setAttribute('tabindex', '-1');
summary.focus();
The summary-with-links pattern (popularised by the GOV.UK Design System) is the gold standard for multi-field forms, because it gives an overview and a route to each individual error.
The WordPress reality
Most WordPress forms come from plugins — Contact Form 7, Gravity Forms, WPForms, Forminator, the comment form, WooCommerce checkout. Their accessibility varies widely, and you usually can’t rewrite their markup directly. What you can do:
- Test the actual rendered errors. Submit each form with bad data and inspect what the plugin emits. Does the error carry
aria-invalidandaria-describedby? Is it in a live region? Many older form plugins show errors only visually. - Prefer plugins with a track record of associating errors programmatically and managing focus. It’s a legitimate selection criterion.
- Mind custom CSS that hides the message. A theme that visually hides plugin error text “to keep things clean” can strip the only accessible signal.
A scanner can catch the structural part of this — a field flagged aria-invalid with no matching describedby, an empty error container, a control with no accessible name. reala11y surfaces these code-level issues in your rendered HTML. What it can’t judge is whether your message text is useful: “invalid input” passes a structural check and still fails the user. That’s the honest boundary — automated tooling, ours included, typically detects roughly 30–40% of WCAG issues by criteria, and message quality lives in the human-review remainder.
The honest takeaway
Accessible validation is four moves: identify the error in text, suggest the fix where you can, wire the message to its field with aria-describedby and aria-invalid, and move focus somewhere useful. Get those right and you’ve covered the mechanics of 3.3.1 and 3.3.3. Then do the thing no tool can do for you: fill your own form out wrong, with the mouse untouched and a screen reader on, and confirm every error is spoken and every fix is clear. For the full criteria, see the WCAG 2.2 guide.