Form Validation UX: When to Show the Error, Not Just How
Most form validation advice obsesses over error styling. The harder question is timing: validate on blur, on change, or on submit? Here's a decision framework that holds up in production.

Most form validation advice obsesses over how the error looks — the red border, the icon, the helper text underneath. That's the easy part. The hard part, the part that actually moves completion rates, is when the error appears. Get the timing wrong and even a beautifully designed form feels like it's nagging the user before they've finished thinking.
This is a breakdown of how we decide validation timing on production forms — checkout flows, sign-ups, onboarding wizards — and the rules we've landed on after too many user testing sessions watching people abandon at the password field.
The three timing modes, and why "on change" is usually wrong
There are really only three moments you can fire a validator:
- On change — every keystroke
- On blur — when the field loses focus
- On submit — when the user tries to send the form
The trap is assuming "more feedback = better UX". On-change validation is the design equivalent of someone reading over your shoulder. The user types j into the email field, gets told it's invalid, types o, still invalid, types h, still invalid. By the time they've typed john@, they've been called wrong four times for doing nothing wrong.
The Nielsen Norman Group has written about this for years and the pattern holds: premature validation feels accusatory. It also creates layout shift as error messages pop in and out, which on mobile pushes the submit button around and tanks accessibility for anyone using a screen magnifier.
The exception that proves the rule
On-change validation does work in two cases:
- Confirming success after a field was already in an error state. If the user has been told the password is too short, switching to on-change feedback once they start fixing it is helpful — they can see when they've crossed the threshold.
- Constraint-style inputs where the input itself shapes the value: password strength meters, character counters, slug generators. These are advisory, not accusatory.
The heuristic: on-change is fine when it's telling the user something new and useful, not when it's pre-emptively scolding them.
A decision framework that holds up
Here's the rule set we use on most projects:
- First interaction with a field → validate on blur. Give the user the courtesy of finishing their thought.
- Field already in an error state → switch to on-change so they get live confirmation when the input becomes valid.
- Submit attempt → validate everything, focus the first error.
- Async validation (uniqueness checks, etc.) → debounce 400–600ms after typing stops, but only after the field has been blurred at least once.
That last rule is the one most teams skip and it matters. Hitting your /check-username endpoint on every keystroke is wasteful and the response races create UI flicker. Waiting until the user has signalled "I'm done with this field" at least once cuts the request volume dramatically.
Implementing it without a state machine nightmare
This is straightforward in React with react-hook-form, which exposes a mode and reValidateMode config that maps almost exactly to the framework above:
import { useForm } from 'react-hook-form';
type SignupForm = {
email: string;
password: string;
};
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupForm>({
mode: 'onBlur', // first validation on blur
reValidateMode: 'onChange', // after error, re-validate on change
});
const onSubmit = async (data: SignupForm) => {
// submit logic
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[^@\s]+@[^@\s]+\.[^@\s]+$/,
message: 'Enter a valid email address',
},
})}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email.message}
</p>
)}
{/* password field follows the same pattern */}
<button type="submit" disabled={isSubmitting}>Create account</button>
</form>
);
}
A few things worth highlighting in that snippet, because they're the accessibility details that get skipped:
noValidateon the form disables the browser's native validation bubbles, which don't match your design system and can't be styled reliably across browsers.aria-invalidis bound to the actual error state, not always-true.aria-describedbyonly points at the error element when an error exists — otherwise screen readers announce "empty" string nodes.role="alert"makes the error announce on appearance without stealing focus.
The submit-time rules that actually matter
When the user hits submit, three things should happen in order:
- Validate every field, even ones the user never focused. This is the moment users find out they missed something.
- Move keyboard focus to the first error. Not the top of the form, not a summary banner — the actual input. This is non-negotiable for keyboard and screen reader users, and it's faster for mouse users too.
- Render an error summary at the top of long forms (more than ~6 fields). Link each summary item to its field via anchor. WCAG 3.3.1 doesn't strictly require this, but for anything longer than a sign-up it's the difference between "I'll fix it" and "I'll close the tab".
The focus-the-first-error step is where most implementations fall down. With react-hook-form you get it via shouldFocusError: true (default). With a custom solution, you need to track field refs and call .focus() after the validation pass — and you need to make sure the field is scrolled into view, because focus without scroll is invisible on mobile.
Async validation deserves its own loading state
If you're checking a username against a backend, the field has three states, not two: valid, invalid, and checking. Don't show a green checkmark while the request is in flight — show a spinner inside the field. We've watched users in testing assume "checking" means "approved" and submit the form, only to bounce back with an error they thought was already resolved.
{status === 'checking' && <Spinner aria-label="Checking availability" />}
{status === 'valid' && <CheckIcon aria-label="Available" />}
{status === 'invalid' && <ErrorIcon aria-label="Not available" />}
Error copy: the part designers leave to engineering
Validation timing only works if the message is worth reading. Two principles we enforce in design reviews:
- Say what's wrong and how to fix it. "Invalid email" is useless. "Email needs an @ symbol" tells the user what to do.
- Never blame the user. "You entered an invalid date" reads as accusatory. "Use the format MM/DD/YYYY" is identical information without the finger-wagging.
For passwords specifically, replace the wall-of-requirements pattern with a live checklist that ticks off as the user types. This is one of the few places on-change feedback is genuinely welcome, because the user is asking for guidance — they know the password isn't done yet.
Things you can stop doing
A short list of patterns we've removed from production forms in the last two years with no measurable downside:
- Disabled submit buttons until the form is valid. Users press the button to find out what's wrong. A disabled button gives no information. Let them submit and route them to the first error instead.
- Inline success ticks on every field. They add visual noise and imply the user needs reassurance for routine actions. Reserve them for genuinely uncertain inputs (async checks, complex format fields).
- Tooltips for validation rules. If a rule matters enough to enforce, it matters enough to be visible. Put it in the helper text under the label.
- Red borders without text. Colour alone fails WCAG 1.4.1. Pair it with text and an icon.
Where we'd start
If you're auditing an existing form, the cheapest win is almost always changing onChange validation to onBlur for first-pass validation, while keeping onChange for already-errored fields. That single change cuts the "yelling at the user" feeling without rewriting your form logic.
After that, instrument it. Track which fields produce the most errors, which fields users edit after their first blur, and where they abandon. Validation timing isn't a design decision you make once — it's a thing you tune against real data. If you want a hand auditing a checkout or onboarding flow, that's the kind of work our product design team does day to day.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Empty States Are Your Best Onboarding Surface — Stop Wasting Them
Most empty states show a sad cloud and a 'No data yet' label. That's a dead pixel. Here's how to turn the zero state into the most persuasive screen in your product.

Skeleton Screens vs Spinners: When Each One Actually Wins
Skeleton screens aren't automatically better than spinners. Here's the decision tree we use on real projects, with code, timing thresholds, and the accessibility traps nobody talks about.

Focus Rings Are Not Optional: A Practical Guide to Visible Focus in 2026
Removing the focus ring is the most common accessibility regression we see in audits. Here's how to keep keyboard users happy without making your designer cry.
