---
title: "Error summaries"
description: "Where the summary goes on a failed submit, what it contains, where focus lands, and why the summary is not a substitute for the inline message."
url: "https://opsinjs.pensievelabs.org/patterns/forms/error-summaries"
source: "https://opsinjs.pensievelabs.org/patterns/forms/error-summaries.md"
section: "Patterns"
kind: "pattern"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["error summary", "form errors on submit", "error list", "focus on error"]
implements: ["field", "alert-banner", "callout", "button"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="pattern" />

## When to use [#when-to-use]

Use an error summary whenever a submit fails and more than one thing could be
wrong. That is any form with more than one field, and also any single-field
form whose failure came from the server rather than the client.

The summary exists for one reason: after a failed submit, a reader who cannot
see the whole page has no way to find out what went wrong or where. Scattering
inline errors down the form solves the *reading* problem for someone scanning it
visually and solves nothing at all for anyone else.

## When not to use [#when-not-to-use]

* **A single field failed validation on blur.** That is an inline error, not a
  summary, and no summary should appear before submit. See
  [Validation timing](./validation-timing.mdx).
* **The failure is not per-field.** "We couldn't reach the server" is not an
  error summary; it is a form-level error message with a retry, and putting it
  in a list of field errors makes the reader hunt for a field that is fine.
* **The form is one question on its own page.** With one question and one error,
  the inline message plus focus on the field is enough. A summary listing one
  item is ceremony. See [Question pages](./question-pages.mdx).
* **You want the wiring.** Mapping server errors back onto controls is
  [Handbook → Forms](../../handbook/forms.mdx).

## How it works [#how-it-works]

<FlowDiagram>
  {`flowchart TD
    A["Reader submits"] --> B{"Any errors?"}
    B -->|"no"| C["Proceed"]
    B -->|"yes"| D["Render the summary above the form, in the DOM before it"]
    D --> E["Move focus to the summary container"]
    E --> F["Each entry links to its field by id"]
    F --> G["Activating an entry moves focus to that control"]
    G --> H["Fixing the field clears its inline error and its summary entry"]
    H --> I{"Any errors left?"}
    I -->|"yes"| J["Summary stays, re-rendered, focus not stolen"]
    I -->|"no"| K["Summary removed and its removal announced"]`}
</FlowDiagram>

The non-negotiable parts:

* **The summary is the first thing in the form, in the DOM.** Not visually
  first and DOM-last. Not in a toast in the corner.
* **Focus moves to the summary on failure.** This is the one place in form
  design where moving focus is correct, because the reader has just taken an
  action and the result of that action is here.
* **Every entry is a link to the field**, using the control's `id` as the
  fragment. Activating it focuses the control, not the label.
* **Entries are in DOM order**, matching the order the reader will meet the
  fields. Ordering by severity makes the list unfindable.
* **Inline errors stay.** The summary is additional, never a replacement. A
  reader who has jumped to field four needs the message there too.
* **The summary updates without stealing focus again.** Re-focusing on every
  keystroke traps the reader in the summary.
* **Server errors join the same summary.** A field rejected server-side appears
  in the list exactly like a client-side failure; the reader should not have to
  learn two error systems.

### The count is a fact, not a scolding [#the-count-is-a-fact-not-a-scolding]

Give the number ("There is a problem" / "There are 3 problems") because it tells
a reader how much work is ahead. Do not add a tone: "Oops! You made 3 mistakes"
turns a form failure into a personal one. That is worse than unhelpful in a
health context, where the form may be about a symptom the reader is worried
about.

## Content [#content]

<DoDont>
  <DoDont.Do>
    Heading: "There are 2 problems". Entries: "Enter your date of birth" and
    "Enter a weight between 20 and 400 kg". Each entry says what to do.
  </DoDont.Do>

  <DoDont.Dont>
    Heading: "Form validation failed". Entries: "dob: required",
    "weight: out of range". Field names and validator names leaking into the UI.
  </DoDont.Dont>
</DoDont>

<DoDont>
  <DoDont.Do>
    An entry whose text is identical to the inline message at the field, so the
    reader recognises it when they arrive.
  </DoDont.Do>

  <DoDont.Dont>
    A short entry in the summary and a different, longer message at the field,
    so the reader has to work out whether these are the same problem.
  </DoDont.Dont>
</DoDont>

Wording rules are owned by
[Error and empty messages](../../content/error-and-empty-messages.mdx).

## Accessibility [#accessibility]

* **The summary is a container with a heading**, and focus moves to that
  container. Give it `tabindex="-1"` so it can receive programmatic focus
  without entering the tab order.
* **Announcement follows naturally from focus.** Moving focus to a heading and
  its list is more reliable across screen readers than relying on a live region
  for content that also changes layout.
* **Entries are real links**, not buttons styled as links, so the reader uses a
  mechanism they already know. Activating an entry lands them at the target.
* **`aria-invalid` and `aria-describedby` are set on every failing control**, so
  the message is available on arrival regardless of how the reader got there.
* **Do not use `role="alert"` on the summary while also moving focus.** The
  duplicate announcement is a common and confusing artefact.
* **Removal is announced.** When the last error clears, a polite status message
  (WCAG 2.2 SC 4.1.3) confirms it; otherwise a screen-reader user has no way to
  know the summary is gone.
* **WCAG 2.2 SC 3.3.1 and 3.3.3** are the floor: the error is identified in
  text, and where the correction is knowable it is suggested.

## Research [#research]

<ResearchNote evidence="opinion" date="2026-09-02">
  The error-summary pattern puts a linked list at the top of the form and
  moves focus to it on failure. It is documented publicly by the GOV.UK
  Design System and the NHS Digital Service Manual, which set out their own
  testing rationale. Read them directly; we cite rather than reproduce, as
  that material is Crown copyright.

  What is ours, and is opinion: the insistence that the summary entry and the
  inline message use identical wording, and the rule against tone in the count.
  The first comes from watching readers arrive at a field and fail to recognise
  that they had reached the right place; the second is a judgement about what a
  health form owes a worried reader.

  What would change our mind: nothing on focus movement. It is the one place
  where moving focus is unambiguously correct. On identical wording, evidence
  that a shorter summary entry improves scanning enough to justify the mismatch.
</ResearchNote>

## Updates to this page [#updates-to-this-page]

<Reviewed />
