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.

W3C, Accessible Name and Description Computation 1.2

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-describedby never points at a removed node.
  • aria-describedby lists 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-invalid in 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), never role="alert".
  • On the field itself, prefer simply clearing the error state — remove aria-invalid, drop the error from aria-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-describedby is 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

Related

The checkbox label pattern covers the name side of the same field; this one covers the description side.