Validation
Patterns for communicating errors and guiding recovery. In a money-transfer app, unclear error states cost trust fast. These patterns standardize how and where errors appear, so users always know what went wrong and how to fix it.
An error in Senco is usually not a typo. It is a rejected account number, a limit that was exceeded, or a document that could not be read — and the user is often on their second attempt already. Every pattern here is optimized for the second attempt succeeding.
Covers
- Inline errors
- Error summaries
- Required states
- Recovery guidance
When validation runs
| Moment | What runs | Why |
|---|---|---|
| While typing | Nothing | An email is invalid for its first six characters |
| On blur | That field's rules | The user has finished with the field |
| On re-entry after an error | That field's rules, live | They are actively fixing it; silence is unhelpful |
| On submit | Every rule, including cross-field | The last chance before the request |
| From the server | Whatever the client could not know | Account number rejected, limit exceeded, rate moved |
The one exception to "never validate on keystroke" is a field that already has an error. Once a user is correcting, live feedback tells them when they have succeeded — which is the moment they want confirmation.
Inline errors
- Appear below the input field
- Include the field name and what went wrong
- Use error color, icon, and text (not color alone)
- Associated with the input via aria-describedby
- Appear on blur or on form submission (not on every keystroke)
Anatomy
| Part | Token |
|---|---|
| Field border | --ds-red-600 |
| Focus ring | --ds-focus-ring-error |
| Icon | --ds-error-icon, 16px |
| Message | --ds-error-text, --ds-text-sm |
| Gap | --ds-space-2 below the field |
The helper text stays above the field when an error appears below it. The helper usually states the format, and removing it at the moment of an error removes the line that explains the fix. aria-describedby lists the error first, so the actionable half is read first.
Inline errors are not announced on appearance
An error that fires an announcement on every blur interrupts a user who has moved on to the next field. The message is attached through aria-describedby and is read when the user returns to the field. Announcements are reserved for submission failure, where the user is waiting for a result.
Try editing the value to see the error appear/disappear.
Error summary
- Appears at the top of the form when submission fails
- Lists all errors with links to the corresponding fields
- Focus moves to the error summary on submission failure
- Each error in the summary matches the inline error message
- Uses role="alert" or aria-live="assertive" for announcement
Why focus has to move
A summary a user never lands on is a list nobody reads. On a long form, a screen reader user who submits and hears nothing has no way to discover that anything failed — and a sighted mobile user is scrolled to the bottom, looking at a button that did nothing.
Moving focus to the summary solves both: it announces itself, and it scrolls into view.
Requirements
| Requirement | Detail |
|---|---|
| Placement | Top of the form, above the first field |
| Heading | "There are 3 problems with this form" — count included |
| Items | One per error, in visual field order |
| Links | Each links to the field id and moves focus there |
| Wording | Identical to the inline message; two wordings for one problem reads as two problems |
| Dismissal | None. It disappears when the errors are fixed |
Server errors join the same summary
An error that only the server can detect — a rejected account number, an exceeded limit, a rate that moved — appears in the same summary in the same format. Users do not distinguish client from server validation, and a second, differently-shaped error surface teaches them to look in two places.
Where a server error belongs to no single field ("This transfer exceeds your monthly limit"), it appears at the top of the summary without a field link.
There are 2 errors in this form
Required states
- Required fields indicated in the label text ("required" or asterisk with legend)
- Optional fields may be marked if most fields are required
- Never rely solely on the asterisk — provide a legend at the top of the form
- Use aria-required="true" on required inputs
Mark the minority
If most fields are required, mark the optional ones. If most are optional, mark the required ones. Marking every field in a form where all nine are required adds nine asterisks that carry no information.
The asterisk is never the only signal — it needs a legend, and an asterisk rendered in red alone would fail SC 1.4.1.
Recovery guidance
- Error messages tell the user how to fix the problem
- Avoid technical jargon ("Invalid input" → "Enter a valid email address")
- Preserve user input so they don't have to re-enter data
- Multi-field errors explain which combination is invalid
Message structure
Every message names what is wrong and what to do. Where the system knows the actual value, it says it — "your balance is 180.00 USD" is more useful than "insufficient funds" because it tells the user what amount would work.
| Do | Don't |
|---|---|
| "Enter an amount between 1.00 and 5,000.00 USD" | "Invalid amount" |
| "This account number is 10 digits — you entered 9" | "Format error" |
| "You don't have enough to send this. Your balance is 180.00 USD." | "Insufficient funds" |
| "The receiving bank rejected this account number. Check it with your recipient." | "Transfer failed (ERR_4021)" |
| "The ID photo was too blurry to read. Try again in better light." | "Verification unsuccessful" |
| "Enter the recipient's name exactly as it appears on their bank account" | "Name mismatch" |
Never expose an error code, a schema field name, or the word "invalid" alone.
Input is never cleared
A validation error preserves everything the user typed. Clearing a field "so they can start fresh" costs them the work, and on a form where the value was transcribed from a physical document it costs them finding the document again.
The same applies to a failed submission: every field on the form retains its value, and the flow's step is retained too.
Errors that are not the user's fault
A network failure, a server error, or a payment rail outage is not a validation error and must not be phrased as one. It says what happened, what is true of the user's money, and what happens next:
We couldn't reach the bank — nothing has been sent and your balance is unchanged. Try again in a few minutes.
The middle clause is the one that matters. A user whose transfer errored does not know whether their money moved, and until they are told, they assume the worst.
Accessibility
- All error messages are perceivable by screen readers
- Focus management guides the user to the first error
- Color is never the sole indicator of an error state
- Error messages are concise and actionable
Implementation checklist
-
aria-invalid="true"on every errored field -
aria-describedbypoints at the error, then the helper text - Error summary uses
role="alert"and receives focus on failed submit - Summary items link to field
ids and move focus, not just scroll - Summary order matches visual field order
- Inline errors are not live regions
- Errors carry icon and text, not just red
- Required marking has a legend
- Field values survive every error path
- Server errors appear in the same surface as client errors
Known gaps
- No pattern for async field validation, including its pending state.
- Server error taxonomy is not documented — which codes map to which user-facing message.
- Offline submission failure is described in Forms but has no specified message.
- Cross-field validation ("these two dates are the wrong way round") has a rule but no worked example.