Accessible forms and dynamic UI · 4 / 8
lesson 4

Error summaries at the top

On a long form, inline errors aren't enough. A summary block at the top gives every user one place to start.

~ 14 min read·lesson 4 of 8
0 / 8

Imagine submitting a 25-field tax form and getting back: red borders on field 4, field 11, field 18, and field 24. Sighted users have to scroll-and-scan. Keyboard users have to tab through the whole form to find them. Screen reader users have to listen to every field. An error summary at the top of the form is the fix: one visible block listing every error, each item a link to the offending field.

The pattern is heavy on judgment — when to use it, when not, and where focus should land — so we'll go slowly.

What the summary looks like

A heading, a short prompt, and a list of errors. Each error is the field's label and a link whose href is the field's id.

summary.html
<div
id="form-errors"
role="alert"
tabindex="-1"
aria-labelledby="form-errors-heading"
>
<h2 id="form-errors-heading">There are 3 problems with your submission</h2>
<ul>
  <li><a href="#email">Enter a valid email</a></li>
  <li><a href="#age">Age must be a number</a></li>
  <li><a href="#terms">You must agree to the terms</a></li>
</ul>
</div>

Three load-bearing details:

  • role="alert" (or a live region equivalent — see below) so the summary is announced when it appears.
  • tabindex="-1" so JavaScript can move focus to the summary even though it isn't a focusable element by default.
  • The heading counts the errors. "3 problems" gives screen reader users an immediate scope.
There are 3 problems • Enter a valid email • Age must be a number • Agree to the terms
The error summary lives at the top of the form. Each list item is an anchor that jumps focus to a specific field below.

Anchors that actually jump

Each summary item should be an <a> whose href is the input's id. Clicking — or pressing Enter on — the link jumps the page and focuses the field, so the user can start typing immediately.

In some browsers, anchor jumps move scroll position but not keyboard focus. Belt and braces:

jump-and-focus.js
document.querySelectorAll("#form-errors a").forEach((link) => {
link.addEventListener("click", (e) => {
  const id = link.getAttribute("href").slice(1);
  const target = document.getElementById(id);
  if (target) {
    e.preventDefault();
    target.focus();
    target.scrollIntoView({ block: "center" });
  }
});
});

The link text matters. "Enter a valid email" is far more useful than "Email" on its own — sighted users can fix the error from the summary alone, without remembering the exact rule.

Where focus goes on submit

Submit fails, validation runs, summary renders. Now what?

Focus the summary container. Not the first link in it, not the first invalid field — the summary block itself, with tabindex="-1" so it's focusable. The screen reader reads the heading ("There are 3 problems with your submission") and the user knows immediately what scale of trouble they're in.

focus-summary.js
form.addEventListener("submit", async (e) => {
e.preventDefault();
const errors = await validate();
if (errors.length) {
  renderSummary(errors);
  document.getElementById("form-errors").focus();
} else {
  form.submit();
}
});

Focus-the-summary vs focus-the-first-invalid-field is a real fork in the road. Pick one per app and stay consistent. Summary is better for long forms (5+ fields, multiple errors); focus-the-field is better for short ones.

Tip

If you focus the summary, make sure the summary itself is visually obvious — bold heading, distinct background, scroll into view. A screen reader user will hear it; a sighted user shouldn't have to scroll to find it.

Live region or not?

role="alert" makes the summary announce itself the moment it appears. That's perfect for the first render after submit. But — gotcha — if the summary block already exists in the DOM and you just refresh its content, some screen readers won't re-announce.

Two clean strategies:

  1. Render the summary fresh each submit. Remove and recreate the node. The new role="alert" triggers fresh.
  2. Move focus into the summary. Focus arrival announces the heading anyway, regardless of role.

Doing both is fine — they reinforce each other.

Watch out

Don't render an empty role="alert" on page load and then populate it later. Some assistive tech caches the empty state and never re-announces.

Keep the inline errors too

The summary doesn't replace inline errors from Lesson 3 — it adds to them. The user clicks an anchor, lands on the field, and the inline message (still wired with aria-describedby) tells them what to fix. Without the inline part, you've sent them to a field with a red border and no instructions.

Think of it as two layers:

  • Summary at the top: the manifest. "Here's everything wrong, in order."
  • Inline at each field: the fix. "Here's what this specific value needs to look like."

Try it yourself

check your understanding
Why does the error summary container need tabindex="-1"?
check your understanding
The links inside an error summary should:
check your understanding
For a 3-field login form with one invalid field, what's the simpler pattern?
check your understanding
Best heading text for the summary block?
check your understanding
Why keep inline errors when you already have a summary?
← prevnext lesson →
KeepLearningcertificate
for completing
Accessible forms and dynamic UI
0 of 8 read