Leverage required, pattern, inputmode, and the Constraint Validation API for robust forms without any external library.
Open this lesson in KodokonThe browser ships with a complete validation engine that many teams reimplement in JavaScript out of habit. The building blocks: required (mandatory field), the types (email, url, date) that validate format, minlength/maxlength, min/max/step for numeric values, and pattern, which accepts a regular expression. Crucial detail: pattern is implicitly anchored - the entire value must match, as if the regex were wrapped in ^ and $. Add autocomplete with the standardized values (postal-code, email, tel): it is both UX and accessibility.
<label for="zip">Postal code</label>
<input id="zip" name="zip" type="text"
required pattern="[0-9]{5}"
inputmode="numeric"
autocomplete="postal-code">The inputmode attribute is often misunderstood: it validates nothing. It merely tells the mobile system which virtual keyboard to display (numeric, tel, email, decimal, search). That is why the type="text" + inputmode="numeric" + pattern combo is the recommended pattern for numeric codes: you get the right keyboard without inheriting the behaviors of type="number".
input:user-invalid {
border: 2px solid #b3261e;
}
input:user-valid {
border: 2px solid #1b5e20;
}On the CSS side, prefer :user-invalid over :invalid: the latter applies as soon as the page loads, painting red fields the user has not even touched yet. On the JavaScript side, the Constraint Validation API rounds out the toolkit: checkValidity() triggers the check, the validity object exposes the exact cause (patternMismatch, valueMissing, tooShort), and setCustomValidity() lets you replace the browser's generic messages with your own.
const zip = document.querySelector("#zip");
zip.addEventListener("input", () => {
if (zip.validity.patternMismatch) {
zip.setCustomValidity("Enter 5 digits.");
} else {
zip.setCustomValidity("");
}
});