All articles
Design & UXOctober 7, 2026 6 min read

Form Validation Timing: When to Show Errors Without Being a Jerk

Validate too early and you shout at users mid-keystroke. Validate too late and they fill out the whole form before learning it's wrong. Here's the timing model we actually ship.

Form Validation Timing: When to Show Errors Without Being a Jerk

Form validation is one of those problems that looks solved until you actually watch a user fill out your signup page. The input turns red before they've finished typing their email. The password rules appear only after they submit. Someone's screen reader announces "invalid" six times in two seconds. Nobody is happy.

This is a timing problem more than a validation problem. The rules are usually fine — it's when you run them that makes the form feel helpful or hostile. Here's the model we use on client projects, with the edge cases that bite us most often.

The three timing modes that actually matter

Every validation trigger reduces to one of three moments:

  1. On change — every keystroke, every selection
  2. On blur — when the field loses focus
  3. On submit — when the user tries to proceed

Most advice online tells you to "use inline validation." That's not a strategy, that's a vibe. The real decision is which rule fires at which moment, per field, based on whether the user has seen the field in a valid state yet.

The core principle: don't tell users they're wrong before they're done

The cleanest rule we've found, borrowed loosely from the GOV.UK Design System and tested across dozens of client builds:

Validate on submit first. Then, once a field has been marked invalid, re-validate it on change so the user sees it turn green the moment they fix it. Validate pristine fields on blur only when the format is unambiguous (email, phone, postcode).

This gives you three distinct user states per field:

  • Pristine — never touched, or touched and left valid. Say nothing.
  • Dirty + valid — user typed something, it's fine. Say nothing, or show a quiet checkmark if the field had a prior error.
  • Dirty + invalid, surfaced — user has been told. Now re-validate aggressively so they get live feedback as they fix it.

Why on-change validation feels aggressive

If you validate email on every keystroke, the user types j and sees "Please enter a valid email." They know that. They're not done. You've just added visual noise and, worse, triggered a screen reader announcement for no reason.

On-change validation is only kind after the user has already been told the field is wrong. At that point they're actively trying to fix it, and instant feedback is exactly what they want. Before that point, it's noise.

The exception is strength meters and character counters — those aren't error states, they're progress indicators, and they belong on change.

Password fields are a special case

Password rules are the one place where showing requirements up-front, before any input, is genuinely helpful. Users need to know the rules before they invent a password, not after. Show the checklist on focus, update it on change, and never mark the field "invalid" until submit.

// Password requirements shown as a live checklist, not errors
const rules = [
  { test: (v: string) => v.length >= 12, label: 'At least 12 characters' },
  { test: (v: string) => /[0-9]/.test(v), label: 'Contains a number' },
  { test: (v: string) => /[^A-Za-z0-9]/.test(v), label: 'Contains a symbol' },
];

<ul aria-live="polite">
  {rules.map(r => (
    <li key={r.label} data-met={r.test(password)}>
      {r.test(password) ? '✓' : '○'} {r.label}
    </li>
  ))}
</ul>

Note the aria-live="polite" — more on that in a moment.

When on-blur is the right call

On-blur validation works for fields where the format is strict and the user clearly intends to be done when they tab out: email, phone, URL, postcode, date-of-birth. If someone types jane@example and tabs away, telling them it's incomplete is useful — they've signaled they're done with that field.

It does not work well for:

  • Required fields that are empty. Marking an empty field as "required" the moment the user tabs through it is punitive. Save required-checks for submit.
  • Fields with async validation (username availability, coupon codes). Debounce on change instead, and show a spinner.
  • Composite fields split across inputs (card number, date pickers with day/month/year). Blur fires between sub-fields and will lie to you.

A practical implementation with React Hook Form

Most React form libraries let you configure this per form, but the defaults are usually wrong. Here's what we actually ship:

import { useForm } from 'react-hook-form';

const form = useForm({
  mode: 'onSubmit',        // first pass: submit only
  reValidateMode: 'onChange', // after error: live feedback
  shouldFocusError: true,  // jump to first error on submit
});

mode: 'onSubmit' with reValidateMode: 'onChange' is the exact pattern described above. For fields that benefit from blur validation (email, phone), override at the field level:

<input
  {...form.register('email', {
    required: 'Email is required',
    pattern: {
      value: /^[^@\s]+@[^@\s]+\.[^@\s]+$/,
      message: 'Enter a valid email address',
    },
    onBlur: () => form.trigger('email'),
  })}
  aria-invalid={!!form.formState.errors.email}
  aria-describedby="email-error"
/>

The onBlur: () => form.trigger('email') promotes just this one field to blur-validation without breaking the global policy.

Accessibility: the part most teams skip

Visual error states are maybe 30% of the problem. The rest is making sure assistive tech users get the same information at the same time, without being spammed.

The three attributes you need

  • aria-invalid on the input, toggled based on error state. Screen readers announce it when the input receives focus.
  • aria-describedby pointing at the error message element, so the error text is read along with the label.
  • aria-live="polite" on dynamic error containers or summary regions, so changes are announced without interrupting.

Use polite, not assertive. Assertive interrupts whatever the user is reading. For form errors, polite is almost always correct — the user will hear the announcement at the next natural pause.

Error summaries for long forms

On forms with more than ~6 fields, a summary at the top on submit is a massive accessibility win and a usability win for everyone:

{Object.keys(errors).length > 0 && (
  <div role="alert" aria-labelledby="error-summary-title">
    <h2 id="error-summary-title">There's a problem with your submission</h2>
    <ul>
      {Object.entries(errors).map(([field, err]) => (
        <li key={field}>
          <a href={`#${field}`}>{err.message as string}</a>
        </li>
      ))}
    </ul>
  </div>
)}

role="alert" is implicitly assertive, which is appropriate here because the user just took an action (submit) and expects feedback.

Error message copy: short, specific, actionable

Timing solves half the problem. Copy solves the other half. A few rules we enforce in review:

  • No "invalid" alone. Invalid how? Say what's wrong.
  • Lead with the fix, not the failure. "Enter a valid email address" beats "Email format is incorrect."
  • Don't shout. No red backgrounds on the entire input. A red border and a message under the field is enough.
  • Keep server errors in the same visual language. If your client-side errors live under the field and your API errors live in a toast, users will miss them.

The edge cases that always come up

Autofill. Browser autofill fires change but often not blur. If your validation runs only on blur, autofilled fields can skip validation entirely. Trigger validation on form mount if any field is already populated.

Paste. Users paste phone numbers with spaces and dashes. Normalize before validating, don't reject.

International formats. Postcodes, phone numbers, and names break every regex you write. If the field is non-critical for your business logic, be permissive. If it's critical (shipping), use a vendor like Google Address Validation or Loqate rather than hand-rolling.

Multi-step forms. Validate the current step on "Next", not the whole form. But remember validation state across steps so going back doesn't lose errors.

Where we'd start

If you're auditing an existing form today, open DevTools, pick the worst-offending field, and ask three questions: when does the error first appear, when does it clear, and what does a screen reader announce? Most forms fail at least one. Fix the timing policy first — move to submit-then-change globally — before you touch copy or styling. The behavioural change alone usually drops support tickets and bumps completion rates, and it costs you a two-line config change in whatever form library you're already using.

#UX#Forms#Accessibility#Frontend

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project