Long before a form submission reaches a server, the browser itself can catch a meaningful share of invalid input — a missing required field, a malformed email address, a number outside an allowed range — using nothing but native HTML attributes. This built-in layer is called constraint validation, and it’s frequently under-used because developers reach for custom JavaScript validation libraries before checking what the platform already provides for free.
Input Types Do More Than Change the Keyboard
The type attribute on an <input> element isn’t just cosmetic. Types like email, url, tel, and number each carry their own built-in validation rules, in addition to triggering the appropriate mobile keyboard layout:
<input type="email" required>
<input type="url">
<input type="number" min="1" max="10">
<input type="tel" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
type="email" rejects a value that doesn’t match a basic email address shape (something, an @, something, a ., something) without any additional attributes. type="url" requires a value that parses as a valid URL. type="number" combined with min and max constrains input to a numeric range, and most browsers pair it with spinner controls for incrementing the value.
required, minlength, maxlength, and pattern
Beyond type, several attributes add specific constraints:
required— the field must have a value before the form can submitminlength/maxlength— bounds on the number of characterspattern— a regular expression the value must match
<input
type="text"
required
minlength="8"
pattern="(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}"
title="At least 8 characters, including a letter and a number">
The title attribute here does real work: when pattern validation fails, many browsers display the title value as part of the validation message shown to the user, so it should describe the requirement in plain language rather than being left blank or filled with a regex explanation no one will read in the moment. Font size for that message text, and for the field’s own label, should generally be set in relative CSS units rather than fixed pixels, for the same accessibility reasons that apply to any other body text on the page.
The Constraint Validation API
Beyond declarative HTML attributes, the Constraint Validation API exposes validation state to JavaScript, for cases where custom messaging or custom validation logic is needed:
const emailInput = document.querySelector('#email');
if (!emailInput.validity.valid) {
if (emailInput.validity.valueMissing) {
emailInput.setCustomValidity('Please enter your email address.');
} else if (emailInput.validity.typeMismatch) {
emailInput.setCustomValidity('That doesn\'t look like a valid email address.');
}
} else {
emailInput.setCustomValidity(''); // clear any prior custom message
}
The validity property returns a ValidityState object with boolean flags for each failure mode: valueMissing (a required field is empty), typeMismatch (doesn’t match the expected type, like email or URL), patternMismatch (fails the pattern regex), tooShort / tooLong (violates minlength/maxlength), rangeUnderflow / rangeOverflow (violates min/max), and valid (true only when every check passes). setCustomValidity() lets a script override the browser’s default message with custom text — and critically, calling it with a non-empty string marks the field invalid even if every native constraint technically passes, which makes it the right tool for validation logic HTML attributes can’t express (like “this field must match the password field above”).
Styling Validation States with CSS
Two pseudo-classes let a stylesheet respond to validation state without any JavaScript: :valid and :invalid. Sizing and spacing on these input states still follow the ordinary rules of the CSS box model — a border-color change on :invalid doesn’t affect layout the way a border-width or padding change would.
input:invalid {
border-color: #c0392b;
}
input:valid {
border-color: #2e7d32;
}
input:invalid:not(:placeholder-shown) {
/* only style as invalid once the user has actually typed something,
avoiding a jarring red border on a fresh, empty required field */
background: #fdecea;
}
The :not(:placeholder-shown) pattern above addresses a common UX complaint: without it, a required field shows as invalid the instant the page loads, before the user has had any chance to fill it in, which reads as the form yelling at the visitor prematurely. Scoping the invalid styling to fields that already have a placeholder present but not shown (meaning the user has typed something) defers the visual feedback to a more appropriate moment.
Preventing Submission Until Valid — And Its Limits
A <form> will not submit if any field’s native constraints fail, and the browser will focus the first invalid field and display its validation message automatically. This is genuinely useful default behavior, and it works without any JavaScript at all.
It is not, however, a substitute for server-side validation. Native HTML validation is enforced by the browser, which means it can be bypassed entirely — through disabled JavaScript, browser developer tools, or a direct HTTP request to the form’s submission endpoint that never goes through the rendered form at all. Every constraint expressed in HTML must be re-checked on the server before the submitted data is trusted or stored. Client-side validation is a legitimate usability feature (fast feedback, no round-trip needed for a typo); it is never a security or data-integrity boundary by itself.
The novalidate Attribute
Adding novalidate to a <form> element disables the browser’s automatic constraint checking entirely, which is useful when a project needs full custom control over validation UX (custom error message placement, validating across multiple related fields together, integrating with a broader form-state library) while still relying on the same HTML attributes and Constraint Validation API to determine validity programmatically:
<form novalidate>
<input type="email" required id="email">
<button type="submit">Submit</button>
</form>
form.addEventListener('submit', (event) => {
if (!emailInput.checkValidity()) {
event.preventDefault();
// display custom error UI here
}
});
This pattern — novalidate on the form, checkValidity() and reportValidity() called manually from a submit handler — is the standard way to get fully custom validation UX while still leaning on native constraint definitions (required, type, pattern, min/max) rather than reimplementing every check by hand in JavaScript.
Autocomplete: A Related, Often-Skipped Attribute
The autocomplete attribute isn’t a validation mechanism, but it belongs in the same conversation, since it directly affects form completion success rates. Setting a correct, specific autocomplete value lets the browser (and any password manager or form-fill tool) correctly populate a field from previously saved information:
<input type="email" name="email" autocomplete="email" required>
<input type="text" name="cc-number" autocomplete="cc-number" inputmode="numeric">
The HTML specification defines a fixed vocabulary of standard autocomplete values — given-name, family-name, email, tel, street-address, cc-number, and dozens more — and using the correct value for a field’s actual purpose (rather than leaving autocomplete unset or, worse, setting autocomplete="off" on fields where autofill is genuinely helpful) is one of the simplest, highest-leverage improvements available for real-world form completion rates, particularly on mobile where manual data entry is slower and more error-prone.
Why Reach for This Before a Validation Library
Third-party form validation libraries add real value for complex cross-field logic and rich custom UI. But a meaningful share of validation needs — required fields, basic format checks, length and range bounds — are fully covered by native attributes and cost nothing in bundle size, work identically across form-heavy pages without any additional script, and degrade gracefully if JavaScript fails to load at all (required and pattern still function; a JavaScript-only validation library does not). Starting from native constraint validation, and adding JavaScript only for what it genuinely can’t express, is the more resilient default.
Frequently Asked Questions
Does the pattern attribute accept full regular expressions?
Mostly, with a couple of differences. pattern uses the same regex syntax supported by the browser’s regex engine but is implicitly anchored — the value must match the entire input, as if wrapped in ^(?:pattern)$ — and it does not support multiline or global flags, since it’s evaluated against a single string value rather than searched within it.
Is client-side HTML validation enough on its own?
No. Native validation is enforced by the browser and can be bypassed by disabling JavaScript, using browser devtools, or submitting a request directly to the server outside the rendered form. Every constraint expressed in HTML must be independently re-validated on the server before the data is trusted.
What does setCustomValidity do exactly?
It lets JavaScript mark a field as invalid with a custom message, overriding whatever the native validation state would otherwise be. Calling it with any non-empty string forces the field invalid, even if every built-in constraint (type, pattern, required, and so on) technically passes — which makes it the tool for validation rules HTML attributes alone can’t express, like matching two password fields.
Why does my required field show a red border before I’ve typed anything?
This happens when a stylesheet applies :invalid styling unconditionally. Since an empty required field is technically invalid from the moment the page loads, it gets styled as an error immediately. Scoping the styling with :invalid:not(:placeholder-shown) defers the visual error state until after the user has interacted with the field.
What does the novalidate attribute do?
It disables the browser’s automatic validation-blocking and default error-message display on form submission, while leaving the underlying HTML constraints (required, type, pattern, and so on) fully queryable through the Constraint Validation API. It’s the standard approach for building fully custom validation UI without abandoning native constraint definitions.
