formsaccessibilityvalidation
One field, three messages — help, error, and success, all associated
A field can carry persistent help text plus a conditional error or success message. Associate them with aria-describedby, toggle aria-invalid, and remember aria-describedby announces on focus — not on change.
The problem
A single field often needs to say three different kinds of thing:
- Help — persistent guidance. "8+ characters, one number." Always relevant.
- Error — conditional. "Password is too short." Only when validation fails.
- Success — conditional. "Username is available." Only when it passes.
The question that started this: how do you associate all of these with the field so a
screen reader conveys them, without them fighting each other? The answer has three moving
parts — aria-describedby, aria-invalid, and a live region — and each does a distinct
job. Getting one right and the others wrong is the common failure.
The three parts
1. aria-describedby — the association
One field can be described by several elements. aria-describedby takes a
space-separated list of IDs, and they are read as one description. Crucially, the order
is the order of the IDs in the attribute, not their order in the DOM:
For each IDREF: Set the current node to the node referenced by the IDREF… Append a space character and the result to the accumulated text.
So aria-describedby="field-error field-help" is announced error first, then help —
regardless of where those elements sit on the page. That ordering is a decision you get to
make, and the attribute is where you make it.
2. aria-invalid — the state
aria-describedby supplies the text. It does not tell assistive tech that the field is in
an error state — that's aria-invalid="true", which makes a screen reader announce the
field as "invalid". Set it when validation fails, clear it (false, or remove) when it
passes. Text and state are two separate signals; ship both.
3. A live region — the announcement timing
This is the part teams miss. aria-describedby is announced when the field receives
focus — not when its content changes. If a user is already focused in the field and you
inject an error into a described element, a screen reader says nothing, because nothing
was refocused.
To announce a message the moment it appears, the message container must be a live
region. Per WAI's forms guidance,
an error revealed dynamically should live in a container with role="alert" (which is an
assertive live region) so assistive tech announces it on change. So the error element does
double duty: referenced by aria-describedby for the on-focus description, and a live
region for the on-change announcement.
Try it
This is one real field. The buttons run the two validation outcomes. The panel underneath
shows the field's live DOM state and the accessible description a screen reader would
build — concatenated in aria-describedby order, per the spec above.
8 or more characters, including a number.
Notice what the accessible description does on each state: error is prepended before help, so the most urgent text is heard first — and the persistent help is still there. On success, the error is swapped out entirely rather than left behind as stale text.
Do
<label for="pw">Password</label>
<input
id="pw"
aria-describedby="pw-error pw-help" <!-- error first, then help -->
aria-invalid="true"
/>
<p id="pw-help">8 or more characters, including a number.</p>
<p id="pw-error" role="alert">Password is too short.</p>
- Keep all message elements in the DOM with stable IDs; toggle their content and
visibility, not their existence, so
aria-describedbynever points at a removed node. aria-describedbylists help plus whichever conditional message is active — error or success, never a stale one.- Order the attribute by urgency: put the error ID before the help ID.
- Error containers are live regions (
role="alert") so they announce on change, not only on focus. - Toggle
aria-invalidin lockstep with the error text.
Don't
<!-- Error only conveyed by colour + an icon: invisible to a screen reader -->
<input id="pw" class="is-invalid" />
<p class="text-danger">✗ Password is too short.</p>
<!-- describedby set, but no live region and no aria-invalid:
if focus stays in the field, nothing is announced, and the field
is not reported as invalid -->
<input id="pw" aria-describedby="pw-error" />
<p id="pw-error">Password is too short.</p>
<!-- Error placed in title: unreliable, not announced by most screen readers -->
<input id="pw" title="Password is too short." />
<!-- describedby points at an element that gets removed from the DOM on
valid input → dangling reference -->
<input id="pw" aria-describedby="pw-error" />
On success messages specifically
This was the contested part of our discussion, so state the position plainly: not every success needs a message, and success should almost never be assertive.
- A green tick that confirms the obvious ("this required field is filled in") is noise. It clutters the accessible description and interrupts for no benefit.
- Success is worth announcing when it confirms something the user could not otherwise
know — "username is available", "coupon applied". Put that in a
role="status"container (a polite live region), neverrole="alert". - On the field itself, prefer simply clearing the error state — remove
aria-invalid, drop the error fromaria-describedby— over adding a permanent "looks good" that lives in the description forever after.
The demo's success case uses role="status" and does not leave success text in
aria-describedby — it swaps the error out and lets the field return to just its help text.
When to validate
WAI identifies three moments, and they are not interchangeable:
| Moment | Good for | Watch out for |
|---|---|---|
| On submit | The reliable baseline; catches everything | Move focus to the first invalid field |
| On blur (focus change) | Format checks — dates, emails | Don't fire while the user is still typing |
| While typing | Live availability checks — usernames | Use aria-live="polite"; never assertive, or you interrupt every keystroke |
Errors summarised at the top of a long form should sit in a container before the form, each item a link to its field, with a heading so users can find it. The per-field association described here and a submit-time summary are complementary, not alternatives.
Why
- SC 1.3.1 Info and Relationships (AA)
— the message-to-field relationship must be programmatic, not just visual proximity.
aria-describedbyis what encodes it. - SC 3.3.1 Error Identification (AA) — errors must be identified in text, not by colour or icon alone.
- SC 3.3.3 Error Suggestion (AA) — where you know the fix, say it ("must include a number"), not just "invalid".
- SC 4.1.3 Status Messages (AA)
— a status conveyed without moving focus (success, live validation) must use a live
region so assistive tech can announce it. This is the criterion behind the
role="alert"/role="status"requirement.
Sources
- W3C — Accessible Name and Description Computation 1.2 —
aria-describedbyis concatenated in attribute order - WAI — Forms tutorial: User Notifications
- MDN —
aria-describedby
Related
The checkbox label pattern covers the name side of the same field; this one covers the description side.