Introduction
Forms are often the most important interaction in a web application. A registration form, checkout flow, or settings page must guide users through errors without losing their input or excluding people who rely on assistive technology.
Reliable form design combines browser capabilities, clear validation rules, accessible feedback, and server-side verification.
1. Start with semantic HTML
Use native form elements before introducing custom controls. They provide keyboard behavior, browser integration, and accessibility semantics.
function ProfileForm() {
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const name = formData.get("name");
console.log({ name });
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Display name</label>
<input
id="name"
name="name"
type="text"
required
minLength={2}
/>
<button type="submit">Save profile</button>
</form>
);
}
The browser can enforce basic constraints such as required and minLength. These checks improve usability, but they are not a security boundary.
2. Validate at the correct layers
A good validation strategy has several parts:
- Browser validation provides immediate feedback for simple constraints.
- Client-side application validation handles more complex interactions and dependent fields.
- Server-side validation enforces the authoritative business rules.
Never trust client-side validation alone. Requests can be modified or sent directly without using the interface.
Keep validation rules consistent where practical, but do not assume that frontend and backend code must share the same implementation.
3. Show errors clearly
An error should identify the field, explain what went wrong, and tell the user how to fix it.
For custom validation, associate the message with its field:
<label htmlFor="email">Email address</label>
<input
id="email"
name="email"
type="email"
aria-invalid={hasEmailError}
aria-describedby={hasEmailError ? "email-error" : undefined}
/>
{hasEmailError && (
<p id="email-error">
Enter a valid email address.
</p>
)}
Here, hasEmailError must be defined by your form state or validation logic. Ensure the error is not communicated only through color; text and accessible semantics should also convey it.
For long forms, consider a summary of errors that links users to the relevant fields.
4. Preserve user input after failures
If the server rejects a submission, avoid resetting the form unnecessarily. Users should not have to re-enter every field because one value was invalid or the network temporarily failed.
Distinguish validation errors from infrastructure failures. “Your email address is invalid” requires a different response from “We could not save your changes; please try again.”
Disable duplicate submissions when appropriate, but do not leave a form permanently disabled if a request fails. Always restore a usable state.
5. Test forms with real interaction patterns
Test more than the happy path. Include keyboard-only navigation, screen-reader-relevant semantics, empty fields, malformed input, server errors, and repeated submissions.
Automated accessibility tools can catch some issues, but manual keyboard testing remains important. Verify that focus moves predictably and that error messages are discoverable.
Conclusion
Accessible forms are easier to use and easier to maintain when they rely on semantic HTML, layered validation, and clear feedback. Build on native browser behavior first, validate authoritative rules on the server, and test failure scenarios as carefully as successful submissions.