Form Validation
Checking user input before submission - HTML-native constraints, JavaScript custom messages, and the server-must-re-validate rule that client validation never replaces.
Form validation checks that user input meets your requirements before it is submitted. The browser handles common cases through HTML attributes alone; JavaScript steps in for cross-field rules, custom error messages, and live feedback. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.
HTML provides built-in constraints through attributes like required, type, minlength, and pattern. The browser enforces them automatically when the form submits, shows native error bubbles, and exposes them via the :invalid CSS pseudo-class for styling.
<form id="signup">
<input
type="email"
name="email"
required
placeholder="you@example.com"
/>
<input
type="password"
name="password"
required
minlength="8"
/>
<button type="submit">Sign up</button>
</form>const form = document.querySelector("#signup");
// Check validity programmatically without submitting
console.log(form.checkValidity()); // false if any constraint fails
// Access individual field validity state
const emailField = form.querySelector("input[type='email']");
console.log(emailField.validity.valueMissing); // true if empty
console.log(emailField.validity.typeMismatch); // true if not a valid email/* Style invalid fields after the user interacts (not on page load) */
input:user-invalid {
border-color: red;
}setCustomValidity lets you inject your own error message into the browser's built-in validity system. Call it with a non-empty string to mark a field invalid, then call reportValidity() to show the error to the user. Clear it with an empty string once the field is valid again.
const form = document.querySelector("#signup");
const password = form.querySelector("input[name='password']");
const confirm = form.querySelector("input[name='confirm']");
form.addEventListener("submit", (event) => {
// Cross-field rule: passwords must match
if (password.value !== confirm.value) {
confirm.setCustomValidity("Passwords do not match.");
confirm.reportValidity(); // shows the native error bubble
event.preventDefault(); // stop the form from submitting
return;
}
// Clear any previous custom error before submitting
confirm.setCustomValidity("");
});Live validation gives instant feedback as the user types. Running it on every input event can feel aggressive, especially for fields like email where the address isn't complete mid-type. A short debounce (waiting for a pause in typing) avoids false errors.
const emailField = document.querySelector("input[type='email']");
const errorEl = document.querySelector("#email-error");
let debounceTimer;
emailField.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (emailField.validity.typeMismatch || emailField.validity.valueMissing) {
errorEl.textContent = "Enter a valid email address.";
emailField.setAttribute("aria-describedby", "email-error");
} else {
errorEl.textContent = "";
emailField.removeAttribute("aria-describedby");
}
}, 400); // wait 400 ms after the user stops typing
});In production
Client-side validation is a UX feature, not a security control. Any user can open the browser console, remove the required attribute, and submit whatever they want. The server must re-validate every input before acting on it. When displaying errors, place the message next to the failing field and connect it with aria-describedby so screen readers announce the error context. Never use alert() for validation feedback - it blocks the UI and is inaccessible.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free