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-invalid on 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:

  1. Same words. Summary link text equals the inline message.
  2. Same source. Both render from one error state; neither computes its own.
  3. Summary only after submit. It never appears on blur.
  4. Summary shrinks as errors are fixed — without stealing focus or re-announcing.
  5. One announcement per event. On submit, focus the summary (which reads it); do not also announce each inline error.
What each component does at each moment On blur with an error the inline error appears and the summary does not exist. When a field is fixed before any submit, the inline error clears. On an invalid submit, inline errors appear for all invalid fields, and the summary appears, lists every error and receives focus. When a field is fixed after submit, its inline error clears and its summary item is removed without moving focus or announcing. On a successful submit, the summary is removed. Event Inline error Summary blur, invalid (before submit) appears — (not shown) field fixed (before submit) clears — submit, invalid shown for all invalid appears, focused, lists all field fixed (after submit) clears item removed, no focus move submit, valid — removed

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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]!));
<div id="error-summary" class="error-summary" tabindex="-1" role="group"
     aria-labelledby="error-summary-title" hidden></div>

Step-by-step walkthrough

  1. Hold errors in one ordered list. Field order, so the summary lists problems top to bottom as they appear on the page.
  2. Render inline errors from that list. Visibility follows the field’s own timing; the message text comes from the list.
  3. 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.
  4. Focus the summary on each invalid submit. Its heading is read, and the list is navigable; do not additionally announce each error.
  5. Remove items silently as fields are fixed. The count in the heading updates; focus stays in the field the user is editing.
  6. 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.

Submit, then fix one field The user presses submit with two invalid fields. The form marks submit attempted, shows both inline errors, renders the summary with two links and focuses it; the screen reader reads there are 2 problems. The user follows the email link, fixes the email, and the inline error clears. The summary removes the email item and its heading becomes there is a problem, but focus stays in the email field and nothing is announced. User Form state Summary Email field submit (2 invalid) render 2 items + focus follow link; type valid email email valid: remove error render 1 item, no focus move

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.

Two views, one truth The shared error state is an ordered list of errors with field, message and target id, plus whether a submit has been attempted. The inline view shows each field's message next to it according to the field's timing. The summary view lists every error as a link after a submit attempt, focuses on submit and shrinks silently as errors are fixed. Error state Ordered errors: field, message, target. submitAttempted. Inline view Per field, by its timing. describedby + aria-invalid. Summary view After submit only. Links; focus on submit; shrinks silently.

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.


Related

← Error Summary and Messaging