Errors People Can Actually Perceive
An error must be programmatically tied to its field, announced when it appears, reachable by focus and expressed in more than colour. Red text is not an error state.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
When a field is wrong, how does every user — including one who cannot see the screen — find out what is wrong and where?
Someone submitted a form and it did not go through. They want to know which field is the problem, what is wrong with it, and how to fix it — without hunting.
Colour the invalid field's border red and show a red message underneath it. It is visually obvious what is wrong.
A screen reader user tabs into the field and hears "Email, edit" — no error, no message. The red text is a sibling element the assistive technology has no reason to associate with the control (The Accessibility Tree).
- A screen reader user tabs into the field and hears "Email, edit" — no error, no message. The red text is a sibling element the assistive technology has no reason to associate with the control (The Accessibility Tree).
- A user with colour vision deficiency sees a border that is a slightly different shade. If colour is the only indicator, roughly one in twelve men will not reliably perceive it (Contrast, Colour and Motion).
- On a long form, the first error is offscreen after submit. The page appears not to have responded at all, and users conclude the button is broken.
- The error appears after an async check and the user is already three fields away. Nothing was announced, so they discover it only on the next failed submit.
- The message is a message toast that disappears on a timer. Anyone reading slowly, using magnification, or listening to a screen reader misses it, and there is no way to get it back.
- Focus stays on the submit button after a failed submit. A keyboard user must tab backwards through the whole form to find out what happened (Keyboard Operability).
What is actually happening
In the browser, not in the framework.
aria-describedbyon the control, pointing at the id of the message element, makes the message part of the control's accessible description. It is read after the label and the role, every time the control is focused — which is what makes the error discoverable later, not only at the moment it appears.aria-invalid="true"puts the control into an invalid state on the accessibility tree, so it is announced as invalid. The nativerequiredattribute and a failing constraint set this implicitly; a custom error does not, so you set it yourself (Native Validation and Its Limits).- Announcement at the moment of change requires either a live region or a focus move. A live region (
role="status"/aria-live="polite", orrole="alert") must already be in the DOM when its content changes; inserting a populated live region is unreliable across screen readers. - Moving focus is the strongest form of announcement: focusing an element causes its name, role, state and description to be read. That is why focusing the first invalid control after a failed submit does most of the work by itself.
- An error summary — a container at the top of the form listing each error as a link to its field — gives a single place to receive the whole outcome. Focusing the summary heading after a failed submit announces the count and lets the user jump directly to each field.
- Text, an icon and a border together make the state perceivable through more than one channel. That redundancy is the actual requirement; "do not use colour alone" is its shorthand.
- Placeholders are not descriptions. They vanish on focus, are not consistently exposed, and cannot carry an error (Semantics Before ARIA).
What this makes the browser do
And which of it is avoidable.
- Recomputing the accessible name and description for the control when
aria-describedbyor the referenced element's content changes, and pushing the update to the platform accessibility API. - Recomputing the accessibility tree node when
aria-invalidflips, which is cheap but is a real update the assistive technology observes. - Layout and paint when the message element is inserted, which shifts every subsequent field down unless space was reserved (Visual Stability).
- Style invalidation for
:user-invalidor an error class, matched against the affected subtree (Style Invalidation). - None of it is expensive. The engineering difficulty here is correctness, not cost.
Associating the message with the field
The markup below is the minimum that makes an error exist for everyone. Two attributes do the work: aria-describedby to attach the text, and aria-invalid to set the state.
The detail that catches people is the id. If the message element is only rendered when there is an error, then aria-describedby must reference it only when it exists — or, better, keep the element in the DOM permanently and change its content, which also keeps the reserved space and avoids a layout shift.
1<div class="field">2 <label for="email">Email address</label>3 4 <!-- describedby lists BOTH ids: the hint survives the error -->5 <input6 id="email"7 name="email"8 type="email"9 autocomplete="email"10 required11 aria-describedby="email-hint email-error"12 aria-invalid="true" />13 14 <p id="email-hint" class="hint">We only use this for order updates.</p>15 16 <!-- Always in the DOM. Empty when valid; space reserved either way. -->17 <p id="email-error" class="error">18 <svg class="error-icon" aria-hidden="true" focusable="false">…</svg>19 Enter an email address, like name@example.com.20 </p>21</div>Three deliberate choices. The description references two ids, so the hint is not lost when the error appears. The error paragraph is always present, so the reference never dangles and the layout never shifts. And the icon is aria-hidden, because it repeats what the text already says — an icon with no text alternative next to an unannounced message is the most common way this markup is got wrong.
What the pattern owes a keyboard and a screen reader
Association makes the error discoverable. Announcement and focus make it findable at the moment it matters. The spec below covers both, and includes the error summary because on any form long enough to scroll, it is the difference between "the button did nothing" and a clear list of what to fix.
The summary is not decoration. It is the only element that can carry the aggregate outcome — how many errors, in which fields — which is information no individual field message contains.
semantics Each message associated by aria-describedby; the control carries aria-invalid="true"; the summary is a container with a heading, made focusable with tabindex="-1", containing a list of links whose hrefs point at the invalid controls' ids.
| Enter (on a summary link) | Moves focus to the invalid control, which announces its label, invalid state and error description. |
| Tab | Reaches every invalid field in order; each announces its own error via the description. |
| Enter (in a text field) | Re-submits, re-runs validation, and re-announces the summary if errors remain. |
| Screen reader browse keys | The summary is reachable as a heading and a list, so it can be found again later without re-submitting. |
- — On failed submit, move focus to the summary heading — not to the first field — so the user hears the whole outcome before navigating to a single part of it.
- — If there is no summary, focus the first invalid control instead. Leaving focus on the submit button is the failure this replaces.
- — Summary links must move real focus, not merely scroll.
href="#field-id"plus a focusable target, or an explicit.focus()call. - — Do not move focus on blur-time validation. Focus moves belong to explicit user actions such as submit (Focus Management).
- — On successful submit without navigation, move focus to the result so the outcome is announced there too (Submission: Method, Encoding and Doing It Once).
- — On focus of an invalid control: label, role, required state, invalid state, then the error text via the description.
- — On submit failure: the summary, via the focus move — "3 errors. Email address: enter an email address…".
- — For an error that appears without a focus change, such as an async availability check: a polite live region that is already in the DOM (Live Regions and Announcement).
- — When an error is resolved, remove
aria-invalidand clear the message so nothing stale is announced on the next visit to the field.
usually broken by The pattern invites announcing on every keystroke. Binding a live region to live validity produces continuous speech, makes the field impossible to complete, and trains users to turn the region off — which then silences the announcement that mattered.
Colour is not an indicator
Every row below is a real pattern that ships regularly and looks correct in a design review. The common thread is that each communicates through exactly one channel — colour, or position, or transience — and each of those channels is unavailable to some users all of the time.
The response column is not "add ARIA". It is usually to add a second channel, or to keep the message present rather than transient.
- Two channels minimum: text plus colour, text plus icon, or all three.
- The message must be available on demand later, not only at the moment it appeared.
- The wording should say what to do next, because that is what the user needs and what translates well (Internationalization).
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Red border only, no text | User sees something is wrong, not what | Colour is the only channel, and it carries no content | Add associated text; keep the border as reinforcement, not as the message. |
Red text near the field, no aria-describedby | Screen reader users hear nothing about the error | Visual proximity is not a programmatic relationship | Associate by id and set aria-invalid (Semantics Before ARIA). |
| Icon with no text alternative | The state is invisible to non-visual users and ambiguous to everyone | Meaning encoded only in a glyph | Put the meaning in the text and mark the icon aria-hidden. |
| Toast notification for a field error | Missed by slow readers, magnifier users and screen readers; cannot be recalled | Transient and spatially detached from the field | Persistent inline message plus a summary at the top of the form. |
| Focus left on the submit button | "Nothing happened when I pressed submit" | No focus move and no announcement | Focus the summary heading, or the first invalid control (Keyboard Operability). |
aria-describedby pointing at a conditionally rendered id | Message visible on screen, never announced | Dangling reference at the moment the attribute is read | Keep the message element mounted and change its content instead. |
| Error announced on every keystroke | Continuous speech; field cannot be completed | Live region bound to live validity state | Announce on blur and on submit; clear silently while typing (Native Validation and Its Limits). |
| Generic banner: "Please fix the errors below" | User scrolls hunting for red | The aggregate carries no field information | List each error as a link to its field, with the field name in the link text. |
How to build it
Most important first.
- Give each message element a stable id and reference it from the control with
aria-describedby. Keep any pre-existing hint id in the list — the attribute takes multiple ids, space-separated. - Set
aria-invalid="true"when the field is in error and remove it when it is fixed. A stale invalid state is worse than none. - Render an error summary at the top of the form on failed submit: a heading, a count, and a list of links to each invalid field. Move focus to the summary heading so the whole outcome is announced at once.
- If there is no summary, focus the first invalid control instead. Never leave focus on the submit button.
- Write messages that say what to do, not what is wrong: "Enter a date in the future" rather than "Invalid date". Include the field name in the summary entries, since they are read out of context.
- Use text plus an icon plus colour. Any two of them surviving alone should still communicate the state.
- Reserve vertical space for the message, or animate the insertion, so the fields below do not jump under a moving pointer.
- Show errors at a useful moment: on blur, on submit, and immediately when an existing error is resolved. Not on every keystroke of a field the user has not finished (Native Validation and Its Limits).
- Render server-returned validation errors through the same mechanism as client errors, keyed by field. If the API returns one string, the UI is forced into a generic banner (Validation Errors: Feedback, Not Verdicts).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- This lesson is entirely accessibility, so the requirements are the design: programmatic association, an invalid state, announcement, focus management, and perceivability through more than colour.
- Every error message must be reachable both at the moment it appears and later, when the user returns to the field. Association handles the second case, announcement handles the first, and only doing one of them is the most common half-fix.
- Error text needs the same contrast treatment as any other text; red-on-white at a small size is frequently below the required ratio, and the icon needs a non-colour distinction too (Contrast, Colour and Motion).
- Keyboard users need the error summary's links to move real focus, not just scroll. Scrolling without focusing leaves the tab order where it was.
- Avoid
aria-errormessageas the sole mechanism unless you have verified support across the assistive technologies your users have;aria-describedbyis the broadly supported route and can carry the same text (The Rules of ARIA). - Do not rely on the native validation bubble for this. It is browser chrome — transient, unstyleable, and not re-readable on demand.
What can go wrong
aria-describedbypointing at an id that does not exist, usually because the message is conditionally rendered and the id only exists when there is an error. A dangling reference is silently ignored, so the message is never read.- Overwriting an existing
aria-describedbythat held a format hint, so the hint disappears the moment an error appears. role="alert"on a container that is inserted already populated. Some screen readers miss the change entirely; a region present in the DOM from the start and then filled is reliable.- Announcing on every keystroke by binding a live region to the live validity state, which produces a torrent of speech and makes the form unusable.
- Moving focus on every validation change rather than only on submit, which yanks the caret away while the user is still typing.
- A toast for a validation error. It is transient, it is far from the field, and it usually cannot be recalled.
- An error summary whose links do not actually focus the field, so the user is told where to go and cannot get there.
aria-invalidleft on after the user corrects the field, so a valid field is announced as invalid indefinitely.- Announcing "form has errors" without saying how many or which. That is a notification, not information.
- An async validation result arriving after the user has edited the field, attaching an error to a value that no longer exists — announce only if the value still matches (Cancelling a Request Nobody Is Waiting For).
- A server error response landing after the user has already submitted again, so the summary shows errors from the previous attempt.
- A live region updated twice in quick succession, where the second update replaces the first before it has been spoken and the user hears only one of two errors.
- Focus moved to the error summary at the same moment a re-render replaces the summary element, so focus falls back to the document body and the announcement is lost.
- Error text is user-visible output. A message that echoes submitted input must be escaped by the framework or explicitly sanitised; injecting a server string as HTML is a straightforward XSS sink (Cross-Site Scripting).
- Server-returned messages should be treated as untrusted content even when they come from your own backend, because they frequently contain a value the user supplied (Sanitization and Trusted HTML).
- Do not let error wording leak existence: "No account with that email" tells an attacker which addresses are registered. "Check your email and password" does not (Error Handling and Information Leakage).
- Do not surface internal validation detail — constraint names, column names, stack context — in a user-facing message. Log it with a correlation id and show the user something actionable (The Error Model: Structure Over Apology).
- The message being accurate is not the same as the rule being enforced. Presentation is client-side; enforcement is not (What the Frontend Is Responsible For in Auth).
- "We show a red border and red text, so it is accessible." Colour plus text near the field is a visual solution. Without association and announcement, it does not exist for a screen reader user.
- "
aria-invalidannounces the message." It announces the state. The message reaches the user through the accessible description or a live region. - "
role=alerton the message is enough." It announces once, when the content changes. It does nothing when the user tabs back to the field later, which is when they actually need it. - "The automated checker passes, so we are done." Checkers verify structural facts. They cannot evaluate whether the announcement happened at a useful time or whether the wording is actionable.
- "Errors should appear as you type so users are guided." Live per-keystroke errors tell users they are wrong before they have finished being right. Blur, submit, and clearing an existing error are the useful moments.
- "Native validation handles all this." It handles the first failure, in browser chrome, in the browser's language, once. It is a good baseline and not a complete answer.
Measuring it, and what changes in the field
- The Accessibility pane on an invalid control: is the description present, is the invalid state set, does the description text match what is on screen?
- A screen reader pass through a deliberately failed submit — VoiceOver, NVDA or JAWS. This is the only measurement that actually answers the lesson's question (Accessibility Testing).
- Keyboard-only submit with errors: where does focus land, can you reach every error, can you get back to the field from the summary?
- Automated checks catch dangling
aria-describedbyids, missing labels and contrast failures. They cannot tell you whether the announcement happened or whether the wording helps. - Support tickets and field-level abandonment: a field with a high correction rate usually has a message that does not say what to do (Analytics Events That Answer a Question).
- On a long form or a small screen, the distance between the submit button and the first error is large, which is what makes the summary pattern worth its cost.
- Under magnification, only a small region of the page is visible at once, so an error far from the field or from the focus point is effectively invisible.
- With
prefers-reduced-motionset, animated error reveals must be replaced with an instant change rather than a slower one. - In a translated interface, message length varies substantially, so reserved space and layout must tolerate a message two or three times longer than the English one (Internationalization).
- For an async or server-side error, the user may have moved on before it arrives, which is exactly when a polite live region earns its place over a focus move.
- The summary pattern is more markup, more state and another thing to keep in sync with the per-field messages. It pays for itself on long forms and is overhead on a three-field one.
- Moving focus on failed submit is decisive and can be disorienting if it happens at an unexpected moment. Restrict it to explicit submits.
- Reserving space for messages costs vertical rhythm on a form that is usually error-free.
- Polite live regions can be missed if the user is interacting elsewhere; assertive ones interrupt. There is no setting that is right in both cases, so the choice depends on whether the error blocks progress.
- Doing this properly means the error path has as much code as the happy path, which is a real cost that reviewers should expect to see rather than treat as over-engineering.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERAL
aria-describedby,aria-invalid, live regions and focus behaviour are specified in ARIA and HTML and are implemented consistently enough that the pattern below is portable. - PLATFORM-SPECIFICScreen readers differ in how they treat live-region updates and how much of a description they read on focus: VoiceOver, NVDA and JAWS each handle a region that is inserted already-populated differently, which is why the region must exist in the DOM before its content changes.
- SPEC-EVOLVING
aria-errormessageis the purpose-built attribute for this and its support has improved unevenly; verify against the assistive technologies your users actually run before relying on it alone, and keeparia-describedbyas the carrier until you have.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — the association and contrast failures here are automatable in CI, while announcement timing and message quality are not; knowing which half a tool can prove is what keeps an accessibility check honest.