Lesson · Intermediate
Enhance forms with JavaScript validation
Add timely client feedback while preserving native controls, server authority, accessible errors, safe values, and honest submission states.
- Published
- Last checked
What this solves
Client validation can catch an incomplete or malformed value before a network round trip and can place guidance beside the field being edited. It improves feedback; it does not establish identity, ownership, authorization, or safety. The server must run the authoritative schema and business rules.
Minimal correct example
const form = document.querySelector('form');
const email = document.querySelector('#email');
const error = document.querySelector('#email-error');
if (
form instanceof HTMLFormElement &&
email instanceof HTMLInputElement &&
error instanceof HTMLElement
) {
form.addEventListener('submit', (event) => {
if (!email.validity.valid) {
event.preventDefault();
email.setAttribute('aria-invalid', 'true');
error.textContent =
'Enter an email address in the format [email protected].';
email.focus();
}
});
}
The HTML should already connect the label and error identifier. Script adds the invalid state only after a real validation attempt.
How browsers actually behave
The constraint validation API exposes validity, validationMessage,
checkValidity(), and reportValidity(). Native messages and focus behavior
vary by browser. The submit event is not the same as calling lower-level
submission methods, and a submitter button can contribute its own name and
value.
Network submission can be slow, rejected, duplicated, or interrupted after the server commits work. Client state must distinguish idle, validating, submitting, successful, and failed outcomes. Disabling controls too broadly can omit values or block correction.
Decisions and trade-offs
Validate on submission as the predictable baseline. Add blur validation when a completed field can receive useful early feedback. Avoid per-keystroke errors for unfinished input and avoid live regions that announce every character.
Use ordinary form submission when a full response provides clear recovery. Intercept with fetch only when inline progress materially improves the task and you implement timeout, abort, server errors, duplicate prevention, focus, history, and a useful no-script path.
Accessibility consequences
Errors need visible text that states what failed and how to correct it, plus a programmatic relationship to the field. A summary helps when several controls fail and can receive focus after submission. Preserve safe values and move focus only to a useful recovery point.
During submission, keep the action label understandable and expose relevant progress without repeatedly announcing it. On success, confirm only after the authoritative response. On failure, retain data, describe whether anything was saved, and provide a retry that does not create duplicates.
Common failures
- Errors appear on initial render. Validation runs before interaction. Delay invalid state until submission or an appropriate completed field.
- The server accepts an unexpected value. Client checks were treated as an allowlist. Validate again on the server.
- A request succeeds but the UI shows failure. The connection ended after commit. Use idempotency where needed and provide a status recovery path.
- A failed request clears the form. State is rebuilt from empty defaults. Retain safe values and correct only the rejected fields.
- A live region speaks every keystroke. Feedback is too aggressive. Announce meaningful errors or thresholds instead.
Verify it
Run empty, malformed, boundary, unexpected, valid, duplicate, slow, offline, timeout, server-error, and post-commit interruption cases. Inspect the request, server response, and logged fields. Complete every path with JavaScript blocked, keyboard only, and a screen reader. Confirm labels, descriptions, invalid states, summary links, focus movement, safe-value retention, 200% zoom, and 320-pixel reflow.
Exercise and next step
Enhance a contact form with submission-time validation and an error summary. Keep native submission working when the script is blocked. Simulate a server rejection after two fields are valid and verify those values remain. Then write the server schema and compare every client rule with its authoritative counterpart.
Practice with duplicate prevention
Add a server-generated idempotency token to a harmless test submission and simulate a double click, browser retry, timeout after commit, and manual retry. Confirm the client never treats a missing response as proof of failure or success, and the server does not create duplicate work. Present a status-recovery path when the outcome is uncertain. This pattern matters whenever submission has a lasting effect, even when the form itself looks simple.
Compare the behavior after a full page reload and after navigating back from the result. Browser form restoration can repopulate values that script state does not know about. Reconcile from the actual controls, avoid resubmitting on history navigation, and keep the recovery copy accurate about whether the server accepted anything.
Test that restoration state in every supported browser profile.
Finally, inspect the accessibility tree before submission, after the first error, and after successful recovery. Labels, descriptions, required state, invalid state, and the error summary must tell the same story as the visible form. Clear obsolete messages when values change so assistive technology does not retain a failure the server would now accept.