Validation summary
Validation summary — the list of errors shown at the top of a form after a submit was rejected, where every item is a link to the field it describes.
How this differs from what it looks like
Not alert
A validation summary is a list of errors with links to the fields that produced them, shown once at the top of a form after a failed submit, and focused so the user lands on it. An alert is any message at all. The difference that matters is the links: an error message that only describes what went wrong leaves the user to find six fields in a long form by hand, and the summary exists precisely so that "Enter an email address" is a link that puts the caret in the email field. If your message has no links to fields, it is an alert; if it does, this component also handles the focus move, which is the part that is otherwise always missed.
Examples
There are 3 problems with this form
<div class="validation-summary" id="signup-errors" tabindex="-1">
<h2 class="validation-summary-title">There are 3 problems with this form</h2>
<ul>
<li><a class="validation-summary-link" href="#email">Enter an email address in the format name@example.com</a></li>
<li><a class="validation-summary-link" href="#password">Your password must be at least 12 characters long</a></li>
<li><a class="validation-summary-link" href="#postcode">Enter a postcode, for example SW1A 1AA</a></li>
</ul>
</div>It only helps if focus moves to it, and that needs one line of script the component cannot supply: tabindex="-1" is here, .focus() after render is yours.
Each item links to the field it names, which is the entire value of the pattern.
Tokens 17
Level 2, declared on .validation-summary itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --validation-summary-icon-color | var(--validation-summary-accent) |
| --validation-summary-icon-size | 1.25rem |
| --validation-summary-item-gap | var(--space-2) |
| --validation-summary-link-color | var(--color-on-surface) |
| --validation-summary-link-font-size | var(--font-size-sm) |
| --validation-summary-title-color | var(--color-on-surface) |
| --validation-summary-title-font-family | var(--font-primary) |
| --validation-summary-title-font-size | var(--font-size-base) |
| --validation-summary-title-font-weight | var(--font-weight-semibold) |
| --validation-summary-accent | var(--color-danger) |
| --validation-summary-accent-size | 3px |
| --validation-summary-bg | var(--color-danger-surface) |
| --validation-summary-color | var(--color-on-surface) |
| --validation-summary-gap | var(--space-3) |
| --validation-summary-padding-block | var(--space-4) |
| --validation-summary-padding-inline | var(--space-4) |
| --validation-summary-radius | var(--radius-box) |
Variants and states
Variants
None. It is one shape, and the page does the rest.
Inside it
.validation-summary-link.validation-summary-title
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:focus:hover
Before you ship it
What you have to do 4 requirements
- Move focus to the container, after it is in the DOM and laid out. A summary nobody is sent to is decoration; there is no CSS property, HTML attribute or server redirect that does this.
- Use
tabindex="-1", not0. The summary is a place you are sent, not a stop on the way through the form. - Do not add
role="alert"as well. Focus already reads the element out; a live region on top of it announces the whole list twice. - Point each item at its field's
id, and give the heading the count.
src/css/components/validation-summary.css · npx mostlycss add validation-summary