Build it in layers: native HTML checks first, custom rules in React, accessible error messages, and a second check on the server.
Native checks. required, type='email', minLength and pattern give you free browser checks and accessibility.
Custom rules. Keep the form values in state, or read them on submit. Compute errors from the values when possible, rather than storing them separately. Decide when to show errors: on submit, and then on blur or change for fields the user has touched. This way people don't see errors before they have typed. Always block submission while the form is invalid.
Accessibility:
- Link each message to its input with
aria-describedby. - Set
aria-invalidon invalid inputs. - Move focus to the first invalid field on submit.
Client validation is only for user experience. The server must validate again, because anyone can send a request without your form. For large forms, libraries like React Hook Form with a schema library such as Zod reduce repeated code.
- Use native constraints like
requiredandtype. - Compute errors from the values, and track which fields were touched.
- Validate on submit, then on blur or change.
- Make errors accessible with
aria-invalid,aria-describedbyand focus management. - Always validate again on the server.