Build a Validation & UX
Validate at the right moments (on blur / on submit, then on change once a field is touched/errored), show clear inline messages near the field, never block typing, surface a submit-time summary, mirror server validation, and make errors accessible (aria-invalid, aria-describedby, focus management).
Good validation UX is about when you validate and how you communicate — not just the rules. The goal: catch errors helpfully without nagging.
Validation timing — the core decision
- Don't validate on every keystroke from the start — flagging "invalid email" while someone is still typing the first character is hostile.
- Validate on blur (or on submit) for the first signal — the user finished with the field.
- After a field has errored, switch to on-change — so they see it clear as they fix it. ("Validate on blur, re-validate on change once touched.")
- Always validate on submit — and block submission if invalid.
- Async validation (uniqueness checks) — debounced, with a pending indicator.
Communicating errors
- Inline, next to the field — not just a list at the top.
- Specific and actionable — "Password needs at least 8 characters," not "Invalid input."
- Visually clear — red border + icon + message; don't rely on color alone.
- Show success sparingly for high-stakes fields (a green check on a confirmed unique username).
- Submit-time summary — on a failed submit, a summary at the top and inline errors; focus/scroll to the first error.
Don't punish the user
- Never block typing — let them type anything; validate after.
- Preserve input on a failed submit — never clear the form.
- Disable vs allow-and-explain — prefer letting them click submit and showing what's wrong over a permanently-disabled button with no explanation (a disabled button gives no feedback about why).
Accessibility (frequently the differentiator)
aria-invalid="true"on errored fields.aria-describedbylinking the field to its error message so screen readers announce it.- Error text in a live region, or move focus to the first error on submit.
- Errors associated with
<label>s and inputs properly.
Server-side validation must mirror client-side
- Client validation is UX; server validation is correctness/security — never trust the client.
- Map server validation errors back onto the specific fields, in the same inline style.
- Handle the case where the server rejects something the client thought was valid.
Tooling
React Hook Form + Zod/Yup: schema-driven rules, mode: 'onTouched' for the blur-then-change behavior, and one schema you can share with the server.
Summary
Validate on blur, re-validate on change once touched, always on submit. Inline, specific, accessible messages. Never block typing, never lose input, always mirror on the server.
Follow-up questions
- •When should validation fire — keystroke, blur, or submit?
- •Should you disable the submit button on an invalid form?
- •How do you make validation errors accessible to screen readers?
- •Why must server-side validation exist even with thorough client validation?
Common mistakes
- •Validating on every keystroke from the first character — nagging the user.
- •Vague messages like 'invalid input' with no guidance.
- •Clearing the form or losing input on a failed submit.
- •No aria-invalid/aria-describedby — errors invisible to screen readers.
- •Trusting client validation and skipping server validation.
Performance considerations
- •Debounce async/expensive validation. With controlled forms, per-keystroke validation can re-render the whole form — React Hook Form's uncontrolled model isolates this. Don't run the full schema on every keystroke for large forms.
Edge cases
- •Async validation still pending when the user hits submit.
- •Server rejects something the client validated as fine.
- •Cross-field validation (passwords match, date ranges).
- •Conditional fields — validate only the visible ones.
Real-world examples
- •A signup form: email validated on blur, password requirements shown live once touched, username uniqueness checked async with a spinner.
- •A checkout form mapping server-side payment validation errors back to the relevant inline fields.