---
title: "Validation timing"
description: "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."
url: "https://opsinjs.pensievelabs.org/patterns/forms/validation-timing"
source: "https://opsinjs.pensievelabs.org/patterns/forms/validation-timing.md"
section: "Patterns"
kind: "pattern"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["when to validate", "live validation", "on blur validation", "inline errors timing"]
implements: ["field", "reading-input", "log-sheet"]
---

> 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]

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 [#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](./units-and-numeric-entry.mdx).
* **You want the code.** Resolvers, schemas and server error mapping are
  [Handbook → Forms](../../handbook/forms.mdx).

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

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

<FlowDiagram>
  {`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"]`}
</FlowDiagram>

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 [#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](../../health/unit-systems.mdx).
* **Do not validate a date of birth into the future silently.** Say what is
  wrong; see [Date of birth](../ask-users-for/date-of-birth.mdx).

## Content [#content]

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

  <DoDont.Dont>
    "Invalid input." appears after the reader has typed the digit `1`.
  </DoDont.Dont>
</DoDont>

<DoDont>
  <DoDont.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.
  </DoDont.Do>

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

Error message wording is owned by
[Error and empty messages](../../content/error-and-empty-messages.mdx). The rule
this page adds is about *timing*, not phrasing.

## Accessibility [#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](./error-summaries.mdx).
* **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 [#research]

<ResearchNote evidence="opinion" date="2026-09-02">
  "Silent until blur, live on correction" is a design opinion, though a widely
  converged-upon one. Several public design systems describe the same shape,
  and the reasoning is mechanical rather than empirical: a live region cannot
  usefully announce a message about an incomplete value.

  The health-specific rules are held more strongly, and the strongest is the
  prohibition on blocking a save because a value is implausible. It is opinion in
  the sense that no study told us; it is not really arguable, because the failure
  mode is a reader unable to record a reading at the exact moment the reading
  matters most.

  What would change our mind on the general rule: evidence that first-time live
  validation reduces completion errors enough to outweigh the announcement cost.
  That evidence would still leave the screen-reader case needing a different
  answer.
</ResearchNote>

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

<Reviewed />
