Accessible forms and dynamic UI · 3 / 8
lesson 3

Inline error messages

An error message that nobody finds is just decoration. Wire it to the input, mark it invalid, and put focus where the user can act.

~ 16 min read·lesson 3 of 8
0 / 8

When a form fails to submit, the user gets two questions in quick succession: "Which field is wrong?" and "What do I do about it?" If your error message answers neither — or answers them only on screen, only on submit, only with a red border — you've shipped a dead-end. We have two ARIA attributes purpose-built for this, and one focus rule that decides whether the user even notices.

This is the lesson where ARIA actually earns its keep. The label work from Lesson 1 is now load-bearing: every wire we run goes back to that accessible name.

aria-invalid: marking the field

aria-invalid="true" flags an input as currently in an error state. Screen readers announce "invalid" alongside the field's name. CSS can target [aria-invalid="true"] for the red border. Default value is false, so flip it on only when the field is actually wrong.

invalid.html
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
aria-invalid="true"
/>

Common mistake: leaving aria-invalid="true" on a field that's been corrected. Toggle it back to "false" (or remove it) the moment the value passes validation again. Stale invalid states make the form sound permanently broken.

Watch out

Don't set aria-invalid="true" the second the input gets focus — only after the user has had a chance to type something and you've actually validated. Validating empty fields on focus is hostile.

aria-describedby: linking the message

aria-invalid says something is wrong. aria-describedby says here's what's wrong, read it. Its value is the id of the element holding the message. Screen readers read the description right after the field's name and required/invalid status.

described.html
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" class="error">
Enter an email like name@example.com
</p>

A screen reader hears "Email, edit, invalid entry, Enter an email like name at example dot com." That's everything they need without leaving the field.

aria-describedby accepts a space-separated list of ids, so you can chain a hint and an error:

chained.html
<input
id="password"
type="password"
aria-describedby="pw-hint pw-error"
aria-invalid="true"
/>
<p id="pw-hint">At least 12 characters.</p>
<p id="pw-error">Too short — you've entered 7.</p>
Tip

Keep the message a short sentence. Screen readers read it in full every time the user re-enters the field, so verbosity gets old fast.

Where the message goes

Two patterns are common:

Inline, directly below the field. Best for most forms. The error sits in the visual flow, right where the user's eyes are.

Inline + summary at top. For long forms — Lesson 4 — you also list every error at the top with anchors. The inline messages still exist; the summary is a sighted-user shortcut.

What you should not do: put the error in a tooltip that appears on hover, in a popup that disappears, or in a toast that flies in from the corner. Errors are persistent. They live next to the field until fixed.

Email nameEnter an email like name@example.comaria-invalid="true" aria-describedby="email-error"
Wired correctly: aria-invalid flags the field, aria-describedby points at the message, and the message sits directly below the input.

Focus on error: where the cursor goes after submit

When the user submits and validation fails, someone has to drive the cursor to the problem. Browsers do this for native required constraints; for any custom or async validation, you do it in JS.

Two strategies:

  1. Focus the first invalid field. Programmatically .focus() it. Screen reader announces the field name, invalid, and the error. User starts typing.
  2. Focus an error summary at the top. Covered in Lesson 4. Best for long forms with many errors.

What you should not do is just slap red borders on six fields and let the user hunt. Sighted users can scan; screen reader users cannot.

focus-first.js
form.addEventListener("submit", (e) => {
const firstInvalid = form.querySelector("[aria-invalid='true']");
if (firstInvalid) {
  e.preventDefault();
  firstInvalid.focus();
}
});
Watch out

Don't move focus while the user is mid-typing in another field. Focus-on-error fires after submit, not on every keystroke. Lesson 5 covers the live-validation case.

What the message should say

The message is the smallest thing on the page and it does the most work. Three rules:

  • Be specific. "Enter a valid email" is worse than "Enter an email like name@example.com."
  • Say what to do, not what went wrong. "Too short — needs 12 characters" beats "Invalid password."
  • Don't shame. "Whoops! Looks like you forgot something silly!" gets old at 11pm with a deadline. Just tell them what's missing.

The message should make sense out of context. Screen reader users hear it without seeing the visual layout, so "this field is wrong" is meaningless — "Enter your email" always works.

Try it yourself

check your understanding
Which attribute marks an input as currently in an error state?
check your understanding
aria-describedby's value is:
check your understanding
When the user submits a form with three invalid fields, what should focus do?
check your understanding
What's wrong with putting the error message inside a tooltip that shows on hover?
check your understanding
Which message is best?
← prevnext lesson →
KeepLearningcertificate
for completing
Accessible forms and dynamic UI
0 of 8 read