Error Summary vs Inline Errors
Should a failed form show its errors next to each field, in a summary at the top, or both? And when both exist, where should focus go after submit? The choice affects how quickly different users find and fix problems: sighted mouse users scan for red, keyboard users follow focus, screen reader users hear what is focused or announced, and users with cognitive disabilities benefit from knowing the scope before they start. This recipe compares the two delivery surfaces on those dimensions, recommends a default — inline errors always, a summary whenever more than one error is possible — and implements the focus rule that switches between them based on the number of errors, all built on the Constraint Validation API and the site’s novalidate plus reportValidity() baseline.
When Each Surface Is Needed
Both surfaces answer different questions, which is why most robust forms use both.
- Inline errors answer “what is wrong with this field, and how do I fix it?” They are needed on every form, because the user must see the message at the moment they edit the field.
- An error summary answers “how many problems are there, and where?” It is needed whenever more than one error can occur at once, especially on long forms, forms where errors may be off-screen, and server-rendered forms where no script moves focus.
- Neither alone is sufficient for longer forms: inline-only makes users hunt; summary-only makes users remember the message while they navigate.
The wider comparison of error surfaces — including toasts and modals, which are usually the wrong choice for field errors — is in inline vs toast vs modal error delivery. The summary component itself is built in building an accessible error summary.
Minimal Working Combined Delivery
const form = document.querySelector<HTMLFormElement>("#application")!;
const summary = document.querySelector<HTMLElement>("#error-summary")!;
type Problem = { field: HTMLElement; message: string };
function validateAll(): Problem[] {
const problems: Problem[] = [];
const seenGroups = new Set<string>();
for (const el of form.elements) {
if (!(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement)) continue;
const message = el.willValidate && !el.validity.valid ? messageFor(el) : "";
renderInline(el, message); // inline: every field, always
if (!message) continue;
const key = el.type === "radio" || el.type === "checkbox" ? `group:${el.name}` : el.id;
if (seenGroups.has(key)) continue;
seenGroups.add(key);
problems.push({ field: el, message });
}
return problems;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const problems = validateAll();
if (problems.length === 0) {
summary.hidden = true;
return form.submit();
}
if (problems.length === 1) {
summary.hidden = true; // single error: go straight there
form.reportValidity(); // focuses and announces the field
return;
}
renderSummary(problems); // several: orient first
summary.hidden = false;
summary.focus();
});
The switch at one error is a judgement call that many design systems make: a summary for a single problem adds a step without adding information, while for several problems the count and list save the user from discovering them one by one. Whatever threshold you choose, keep it consistent across the product so users learn what to expect.
Delivery Option Reference
| Aspect | Inline error | Error summary |
|---|---|---|
| Location | Beside or above its field | First element in the form |
| Linked by | aria-describedby on the field |
Links with href="#field-id" |
| Announced when | Field is focused or explored | Summary receives focus / alert role |
| Updates | Live as the field is fixed | On each submit attempt |
| Required for | Every error | Forms where several errors can occur |
| Works without JS | Yes, if server-rendered | Yes, if server-rendered |
| Message text | Canonical | Identical to the inline text |
Verification Steps
import { test, expect } from "@playwright/test";
test("one error focuses the field, several focus the summary", async ({ page }) => {
await page.goto("/application");
await fillAllExcept(page, ["Email address"]);
await page.getByRole("button", { name: "Submit application" }).click();
await expect(page.getByLabel("Email address")).toBeFocused();
await expect(page.locator("#error-summary")).toBeHidden();
await page.getByLabel("Full name").fill("");
await page.getByRole("button", { name: "Submit application" }).click();
await expect(page.locator("#error-summary")).toBeFocused();
await expect(page.locator("#error-summary h2")).toHaveText("There are 2 problems");
});
Edge Cases and Failure Modes
Summary-only designs. Some forms list errors at the top and leave fields unmarked, forcing users to scroll back and forth. Always render inline messages as well.
Inline-only on long forms. Without a summary, a screen reader user hears the first error, fixes it, submits, hears the second, and so on — a loop that can take many round trips on a long form.
Different wording in the two places. Generate both from the same function; different phrasings make users think there are more problems than there are.
Summary that duplicates announcements. If the summary has role="alert" and receives focus and each inline error has role="alert", screen readers may read everything two or three times. Use alert on the summary only, and link inline errors with aria-describedby instead.
Measuring Which Delivery Works
If you are unsure whether a summary helps your forms, measure it. Two metrics are informative: the number of submit attempts before success (a summary should reduce it on long forms, because users fix every problem in one pass) and the time between a failed submit and the next attempt (which should fall when users can see the scope immediately). Compare a variant with the summary against one without, on the same form, and watch abandonment at the failed-submit moment in particular.
Short Forms Need Less
On a two- or three-field form — a sign-in, a newsletter box, a search filter — a summary is usually unnecessary: every field is visible at once, errors are rarely multiple, and reportValidity() taking the user to the first invalid field is enough. The exception is sign-in, where the error is deliberately form-level (“Email or password is incorrect”) and belongs in a single alert region above the fields rather than at either field. Reserve the full summary for forms long enough that users cannot see all fields at once, or for any form whose server-rendered error page must work without script.
Server-Rendered Pages Need the Summary Most
On a page rendered by the server after a failed post, there is no focus management unless you add it, and screen reader users land at the top of a new page. The summary — first in the form, with an “Error:” page title and alert semantics — is what tells them that the submission failed and where to go. That makes the summary non-negotiable for progressively enhanced forms, even if the scripted path uses the single-error shortcut described above. The server-rendered markup is shown in building an accessible error summary, and the no-script flow in progressive enhancement without JavaScript.
Inline Messages for Different Users
Inline messages carry most of the load, so their details matter for every group of users. For sighted users, the message must be close to the field, visually distinct in more than colour, and stable in position so the layout does not jump — see reserving space for error messages to prevent layout shift. For screen reader users, it must be referenced by aria-describedby so it is read with the field, and the field must carry aria-invalid="true" while the message is shown. For keyboard users, it must not insert focusable elements between fields unnecessarily. For users with cognitive disabilities, it must say how to fix the problem in plain words, not just that something is wrong. A summary built on top of inline messages that meet these criteria inherits their quality; a summary cannot compensate for inline messages that miss them.
Frequently Asked Questions
Should I use an error summary or inline errors?
Use inline errors on every form, and add an error summary whenever more than one error can occur at once. They answer different questions: what is wrong with this field, and how many problems are there overall.
Where should focus go after a failed submit?
With one error, move focus directly to that field. With several, move focus to the error summary so users hear the count and list first. Apply the rule consistently across your product.
Should summary items use the same wording as inline errors?
Yes. Identical text lets users recognise the problem when they arrive at the field. Generate both from the same message function.
Can the summary replace inline errors on a long form?
No. Without inline messages, users must remember each message while navigating. The summary is an index to the inline errors, not a substitute for them.
Related Guides
- Inline vs Toast vs Modal Error Delivery — the wider comparison of surfaces.
- Building an Accessible Error Summary — the summary component.
- Linking Errors with aria-describedby — inline error association.
- Managing Focus After Validation Failure — focus rules in depth.