Form Validation
Validating user input is one of the oldest jobs on the web. HTML5 gave us a surprisingly capable Constraint Validation API built right into the browser — required, pattern, type="email" and friends — backed by JavaScript hooks for custom rules and messages. Used well, you can ship robust client-side validation with very little code, and combine it with a server check for safety.
What the browser already does for free
Several HTML attributes turn into validation rules automatically. The browser refuses to submit a form whose fields violate them and shows a default bubble with the failing message.
required— field must not be empty.type="email"/type="url"/type="number"/type="date"— value must parse as that type.min,max,step— numeric / date bounds.minlength,maxlength— string length bounds.pattern="[A-Z]{3}\\d{3}"— custom regex (anchored, applied to the whole value).
signup.html
<form id="signup">
<input name="email" type="email" required />
<input name="age" type="number" min="13" max="120" required />
<input name="username" pattern="[a-z0-9_]{3,16}" required />
<button>Create account</button>
</form>The validity object
Every form control exposes input.validity — a ValidityState object with a boolean for each kind of failure. The summary flag valid is true only if everything else is false.
const email = form.elements.email; email.validity.valueMissing; // true if empty and required email.validity.typeMismatch; // true if type="email" and value is malformed email.validity.patternMismatch; email.validity.tooShort; email.validity.tooLong; email.validity.rangeUnderflow; email.validity.rangeOverflow; email.validity.stepMismatch; email.validity.customError; // set by setCustomValidity email.validity.valid; // overall verdict
The matching message lives at input.validationMessage and is localised by the browser.
Triggering validation from code
Two methods let you check validity programmatically:
form.checkValidity()/input.checkValidity()— returns boolean. Silent.form.reportValidity()/input.reportValidity()— returns boolean and shows the native error bubble on the first invalid field.
form.addEventListener("submit", (e) => {
if (!form.checkValidity()) {
e.preventDefault();
form.reportValidity(); // surface the first error
return;
}
// proceed: form is valid
});Custom messages with setCustomValidity
Default messages are fine but generic. input.setCustomValidity(msg) overrides them. Setting an empty string clears the custom error and lets the built-in rules take over again.
custom message for a password field
const pw = form.elements.password;
pw.addEventListener("input", () => {
if (pw.value.length < 8) {
pw.setCustomValidity("Use at least 8 characters.");
} else if (!/[0-9]/.test(pw.value)) {
pw.setCustomValidity("Include at least one number.");
} else {
pw.setCustomValidity(""); // clear — value is acceptable
}
});Cross-field validation
HTML attributes only know about one field at a time. For "passwords must match" and similar rules, drop into JavaScript on input events.
const pw = form.password;
const confirm = form.confirmPassword;
function checkMatch() {
if (confirm.value !== pw.value) {
confirm.setCustomValidity("Passwords do not match.");
} else {
confirm.setCustomValidity("");
}
}
pw.addEventListener("input", checkMatch);
confirm.addEventListener("input", checkMatch);Styling invalid fields
CSS exposes the validation state through pseudo-classes — :valid, :invalid, :required, :optional, :user-invalid (modern). The last one is especially useful because it only applies after the user has interacted, avoiding the "everything is red on page load" look.
suppress :invalid styling until first submit
form.addEventListener("submit", () => form.classList.add("was-submitted"));paired CSS
/* .was-submitted :invalid { border-color: crimson; } */The invalid event
When a field fails validation during a submit, the browser fires an invalid event on it. You can listen for it to display custom UI instead of the native bubble.
for (const field of form.elements) {
field.addEventListener("invalid", (e) => {
e.preventDefault(); // suppress the default bubble
const msg = field.validationMessage;
field.nextElementSibling.textContent = msg; // your inline error slot
});
}Opt out with novalidate
If you want to bypass HTML5 validation entirely — common for "save as draft" buttons or fully custom validation — add novalidate to the form.
<form novalidate> <!-- the browser will not block submission --> </form>