Forms that add an error summary on top of inline errors often end up with two systems that disagree: the summary says “Enter a valid email” while the field says “Email is invalid”, the summary still lists an error the user fixed a minute ago, and a screen reader hears every message twice after submit.
Inline errors and a summary do different jobs. Inline errors explain what is wrong with this field, where the user is looking and typing. The summary answers what is wrong with the form, and where — once, after a failed submit, as a list of links. Used together, with one source of truth and clear timing, they complement each other. This page, part of error summary and messaging, coordinates them.
Context and prerequisites
The two components’ responsibilities:
- Inline error — next to the field, linked with
aria-describedby,aria-invalidon the field. Appears on blur (for touched fields) and on submit; clears as soon as the field is fixed, per reward early, punish late. - Error summary — at the top of the form (or page), appears only after a submit attempt, receives focus, contains a heading (“There are 3 problems”) and a list of links to each invalid field or group. Built as in building an accessible error summary.
Coordination rules:
- Same words. Summary link text equals the inline message.
- Same source. Both render from one error state; neither computes its own.
- Summary only after submit. It never appears on blur.
- Summary shrinks as errors are fixed — without stealing focus or re-announcing.
- One announcement per event. On submit, focus the summary (which reads it); do not also announce each inline error.
The core pattern: one error state, two views
export interface FieldError { field: string; message: string; targetId: string }
export interface ErrorView {
errors: FieldError[]; // single source of truth, in form order
submitAttempted: boolean;
shownInline: Set<string>; // fields whose inline error is visible
}
// Inline: visible if touched-and-invalid or after submit.
export const inlineMessage = (v: ErrorView, field: string) =>
v.shownInline.has(field) || v.submitAttempted ? v.errors.find((e) => e.field === field)?.message ?? null : null;
// Summary: exists only after a submit attempt, and only while errors remain.
export const summaryItems = (v: ErrorView) => (v.submitAttempted ? v.errors : []);
export function renderSummary(container: HTMLElement, v: ErrorView, focusOnRender: boolean) {
const items = summaryItems(v);
if (items.length === 0) { container.hidden = true; container.innerHTML = ""; return; }
container.hidden = false;
container.innerHTML = `
<h2 id="error-summary-title">There ${items.length === 1 ? "is a problem" : `are ${items.length} problems`}</h2>
<ul>${items.map((e) => `<li><a href="#${e.targetId}">${escapeHtml(e.message)}</a></li>`).join("")}</ul>`;
// Focus ONLY on a submit attempt. Updates while fixing fields must not move focus.
if (focusOnRender) container.focus();
}
export function onSubmit(v: ErrorView, summaryEl: HTMLElement): ErrorView {
const next = { ...v, submitAttempted: true };
renderSummary(summaryEl, next, next.errors.length > 0);
return next;
}
export function onFieldFixed(v: ErrorView, field: string, summaryEl: HTMLElement): ErrorView {
const next = { ...v, errors: v.errors.filter((e) => e.field !== field) };
renderSummary(summaryEl, next, false); // shrink silently
return next;
}
const escapeHtml = (s: string) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]!));
<div id="error-summary" class="error-summary" tabindex="-1" role="group"
aria-labelledby="error-summary-title" hidden></div>
Step-by-step walkthrough
- Hold errors in one ordered list. Field order, so the summary lists problems top to bottom as they appear on the page.
- Render inline errors from that list. Visibility follows the field’s own timing; the message text comes from the list.
- Render the summary from the same list, only after submit. Before the first submit attempt, the summary does not exist — even if fields are invalid.
- Focus the summary on each invalid submit. Its heading is read, and the list is navigable; do not additionally announce each error.
- Remove items silently as fields are fixed. The count in the heading updates; focus stays in the field the user is editing.
- Remove the summary when the form becomes valid or submits successfully. A summary saying “There are 0 problems” is noise.
When a summary is not needed
For a form with two or three fields visible at once — a login form, a newsletter signup — a summary adds little: the user can see every error at a glance, and moving focus to the first invalid field is enough. Summaries earn their place when forms are long enough to scroll, when errors can be off-screen, when fields sit in collapsed sections or other steps, or when server-side errors may relate to fields the user is not looking at. A reasonable rule is: if the first invalid field might not be visible when the user presses submit, use a summary.
Failure modes and edge cases
1. Different wording in each place
Hand-writing summary text (“Email is required”) separately from inline text (“Enter your email address”) confuses users who compare them. Render both from one message.
2. Summary appearing on blur
Showing the summary before any submit attempt turns it into a live, shifting list above the form — layout jumps and noise. Keep it strictly post-submit.
3. Summary links that do not land in the field
Links to a wrapper div or a collapsed section scroll without focusing. Target the input’s id (or the first option in a group), and reveal hidden sections first, as in moving focus to the first invalid field.
4. Double reading on submit
Focusing the summary reads it; a live region announcing “2 errors” as well reads it again. Use focus, not a live region, for the submit event.
5. Server errors without a field
Form-level server errors appear in the summary without a link, and optionally in a banner — they have no inline home, per modelling form-level vs field-level errors.
Verification checklist
Frequently Asked Questions
Should the summary update live after submit?
Yes, silently: remove fixed items and update the count so the summary stays truthful if the user scrolls back. Do not announce those updates; the user knows they fixed the field.
Where should the summary go on a multi-step form?
At the top of the current step for that step’s errors. On a final review step, a summary can list errors from earlier steps with links that take the user back to them, as in building a review step before final submit.
Is role="alert" appropriate for the summary?
Moving focus to the summary already causes it to be read. Adding role="alert" can make some screen readers read it twice. Use a focusable container with a heading, and reserve alert for messages that appear without a focus move.