opsinjs
ComponentsPatternsForm design

Validation timing

When a check should run. Why validating on every keystroke is both the most common choice and the worst one for the readers who most need help.

When to use

Read this when deciding what triggers a validation message in any form. That is every form, since the default behaviour of most form libraries is a decision somebody made for you.

The three candidate moments are:

  1. While typing (on change / every keystroke)
  2. On leaving the field (on blur)
  3. On submit

Almost every product picks (1) because it feels responsive. It is the worst of the three for anybody using a screen reader, because a live region that updates on every keystroke either interrupts the reader's own typing echo or is throttled into uselessness, and it is actively hostile for a reader who types slowly: they are told they are wrong four times while entering a value that will be correct in two more characters.

When not to use

  • The check is not a validation. Showing remaining characters, or a password strength meter, is feedback, not an error, and it is governed by different rules. Never announce it assertively.
  • The value cannot be wrong. A field with a constrained control does not need validation; it needs a control that cannot produce an invalid value.
  • The value is a health reading. A surprising physiological value is not invalid. See below, and Units and numeric entry.
  • You want the code. Resolvers, schemas and server error mapping are Handbook → Forms.

How it works

The opsinjs rule is validate on blur for the first time, then live on correction, always on submit.

Diagram source (mermaid)
flowchart TD
A["Reader focuses a field"] --> B["No validation while first typing"]
B --> C{"Reader leaves the field"}
C -->|"empty and optional"| D["No message"]
C -->|"valid"| E["No message; do not announce success"]
C -->|"invalid"| F["Show the error, associate it, announce politely"]
F --> G["Reader returns to fix it"]
G --> H["Now validate live, so the error clears as soon as it is right"]
H --> I["Submit: re-check everything, summarise remaining errors"]

Why this shape:

  • Nobody is told they are wrong about something they have not finished saying. The first pass is silent.
  • Once a reader knows a field is wrong, live feedback is genuinely helpful. They are now working at the error, and the message disappearing is the signal they wanted.
  • Submit is the backstop, because a reader can reach submit without ever blurring the last field, and because server-side checks only exist there.

Health-specific rules

  • Never block a save on a value being implausible. A reading of 210/130 is alarming and may be true. Ask ("that is much higher than your usual. Is that right?"), record whichever answer the reader gives, and let the status axis do its job. Refusing to store a true value is the single most damaging thing a health form can do.
  • Distinguish impossible from surprising. A negative weight is impossible and can be rejected. A weight 12 kg below yesterday's is surprising and must not be. Only the first is a validation error.
  • Unit confusion is a validation case in its own right. A glucose value that is plausible in mg/dL and implausible in mmol/L should ask which the reader meant, not reject. See Unit systems.
  • Do not validate a date of birth into the future silently. Say what is wrong; see Date of birth.

Content

Do

"Enter your height in centimetres, for example 168." It says what is wanted, with an example, when the reader has finished typing.

Don’t

"Invalid input." appears after the reader has typed the digit 1.

Do

"138 mmol/L is much higher than a usual glucose reading. Did you mean 138 mg/dL?" with both options as buttons and the reader's own entry preserved.

Don’t

"Value out of range" with a disabled save button, so a reader with a genuine emergency reading cannot record it.

Error message wording is owned by Error and empty messages. The rule this page adds is about timing, not phrasing.

Accessibility

  • An error message is associated with its control via aria-describedby, so it is read when the control receives focus rather than only when it appeared.
  • aria-invalid is set when the error is shown and removed when it clears. A field that stays aria-invalid after correction tells a screen-reader user the form is still broken.
  • First-time announcement is polite (WCAG 2.2 SC 4.1.3). Assertive announcements interrupt the reader's own typing feedback and are almost never justified for a single field.
  • Never move focus on validation. Focus movement mid-typing loses the reader's place; the exception is the submit-time error summary, which is covered in Error summaries.
  • Do not announce success per field. "Valid" on every blur is noise that buries the one message that mattered.
  • Colour is never the error. WCAG 2.2 SC 3.3.1 requires the error be identified in text; a red border alone fails, and so does a red border plus an icon with no accessible name.
  • Debounce is not a fix. A 500ms debounce on a live region still produces an announcement mid-word for a slow typist. The fix is not validating until blur.

Research

Updates to this page

Last read through against the system on 2026-09-20. Due for review every 12 months; expiry is reported by pnpm run check:freshness.

On this page