Master built-in HTML form validation with required fields, patterns, constraints, and learn how to provide custom validation messages without JavaScript frameworks.
HTML provides powerful validation without any JavaScript. When a form is submitted, the browser checks each field against its validation attributes and blocks submission if any field is invalid.
| Attribute | Applies to | Description |
|---|---|---|
required | All inputs | Field must not be empty |
minlength / maxlength | Text inputs | Min/max character count |
min / max | Number, date, range | Min/max numeric or date value |
step | Number, range | Valid increment (e.g., step="0.01" for cents) |
pattern | Text inputs | Regular expression the value must match |
type | All inputs | Built-in validation for email, url, number, date, etc. |
The browser shows native error tooltips when validation fails. These messages are localised to the user's language.
The pattern attribute takes a regular expression (without delimiters) and validates the input value against it. The pattern must match the entire value (it is implicitly anchored with ^ and $).
<!-- Phone: digits, spaces, dashes, and optional + prefix -->
<input type="tel" pattern="\+?[\d\s-]{7,15}" title="Phone number">
<!-- Postal code (US): 5 digits or 5+4 format -->
<input type="text" pattern="\d{5}(-\d{4})?" title="US ZIP code">
<!-- Username: 3-20 alphanumeric characters -->
<input type="text" pattern="[a-zA-Z0-9]{3,20}" title="3-20 alphanumeric characters">
<!-- No spaces allowed -->
<input type="text" pattern="\S+" title="No spaces allowed">Always pair pattern with the title attribute — its text is included in the browser's error tooltip to help the user understand the expected format.
CSS provides pseudo-classes that let you style form elements based on their validation state:
:valid — The field passes all validation constraints.:invalid — The field fails at least one constraint.:required — The field has a required attribute.:optional — The field does not have required.:in-range / :out-of-range — For number/date inputs with min/max.:placeholder-shown — The placeholder is currently visible (field is empty).input:valid { border-color: green; }
input:invalid { border-color: red; }
input:required { border-left: 3px solid orange; }Note: Empty optional fields match :valid, and empty required fields match :invalid as soon as the page loads. To avoid styling empty fields on load, combine with :not(:placeholder-shown) or use the :user-invalid pseudo-class (newer browsers).
You can override the browser's default error messages using JavaScript's Constraint Validation API:
const input = document.getElementById('username');
input.addEventListener('input', () => {
if (input.validity.tooShort) {
input.setCustomValidity('Username must be at least 3 characters.');
} else if (input.validity.patternMismatch) {
input.setCustomValidity('Only letters and numbers are allowed.');
} else {
input.setCustomValidity(''); // Reset — field is valid
}
});input.setCustomValidity(message) — Sets a custom error message. Pass '' to clear.input.reportValidity() — Triggers validation UI immediately.input.checkValidity() — Returns true/false without showing UI.input.validity — An object with boolean properties: valueMissing, typeMismatch, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, customError.novalidate on <form> — Disables all validation for that form.formnovalidate on <button> — Disables validation for that specific submit button (useful for "Save draft" buttons).<form>
<!-- Required email -->
<label for="email">Email (required):</label>
<input type="email" id="email" name="email" required
placeholder="you@example.com">
<!-- Username with pattern -->
<label for="username">Username (3-20 alphanumeric):</label>
<input type="text" id="username" name="username"
pattern="[a-zA-Z0-9]{3,20}"
title="3 to 20 letters or numbers"
minlength="3" maxlength="20" required>
<!-- Age with min/max -->
<label for="age">Age (18-120):</label>
<input type="number" id="age" name="age"
min="18" max="120" required>
<!-- Submit and Save Draft -->
<button type="submit">Register</button>
<button type="submit" formnovalidate>Save Draft</button>
</form>What happens when you add the `novalidate` attribute to a <form> element?