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.
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.
<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.
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:
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.
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.
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:
- Render the summary fresh each submit. Remove and recreate the node. The new
role="alert"triggers fresh. - Move focus into the summary. Focus arrival announces the heading anyway, regardless of
role.
Doing both is fine — they reinforce each other.
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."