Difficulty: Intermediate
Form validation ensures that users provide data in the expected format before it is submitted to the server. HTML5 introduced built-in constraint validation that works without JavaScript, using attributes like required, pattern, minlength, maxlength, min, max, and type-based validation. This client-side validation provides immediate feedback and reduces unnecessary server requests, but it should always be paired with server-side validation because client-side checks can be bypassed.
The required attribute is the simplest validation constraint. When present on an input, the browser prevents form submission and shows a validation message if the field is empty. For checkboxes, required means the box must be checked. For radio button groups, at least one radio button in the group must have required, and the group must have a selection. For <select> elements, the first <option> must have an empty value for required to work correctly.
The pattern attribute accepts a regular expression that the input's value must match for the field to be valid. The regex is anchored by default -- it must match the entire value, not just a substring. Common patterns include [A-Za-z]{2,} (letters only, at least 2 characters), [0-9]{6} (exactly 6 digits), and [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} (email format). The title attribute should accompany pattern to provide a human-readable description of the expected format, which browsers display in the validation tooltip.
The minlength and maxlength attributes constrain the number of characters. Unlike min and max (which work with numeric values), these operate on the character count of text input. The browser prevents typing beyond maxlength and shows a validation error if the value is shorter than minlength upon submission. These attributes work on text, email, password, tel, url, and textarea elements.
The Constraint Validation API provides JavaScript methods for custom validation logic. The setCustomValidity() method on an input element sets a custom error message; passing an empty string clears the error and marks the field as valid. The checkValidity() method returns true if the element satisfies all constraints. The reportValidity() method checks validity and displays the native validation UI. The :valid and :invalid CSS pseudo-classes allow you to style inputs based on their validation state, and :user-valid/:user-invalid (newer) only activate after user interaction.
<form>
<label for="username">Username (3-20 chars, alphanumeric):</label>
<input type="text" id="username" name="username"
required minlength="3" maxlength="20"
pattern="[a-zA-Z0-9_]+"
title="3-20 characters, letters, numbers, and underscores only">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="age">Age (18-99):</label>
<input type="number" id="age" name="age"
required min="18" max="99">
<label for="bio">Bio (10-500 characters):</label>
<textarea id="bio" name="bio"
required minlength="10" maxlength="500"></textarea>
<button type="submit">Submit</button>
</form>
Each field uses appropriate validation attributes. The browser enforces these constraints automatically and shows native error messages when validation fails. No JavaScript is needed for basic validation.
<form id="signup-form">
<label for="pwd">Password:</label>
<input type="password" id="pwd" name="password"
required minlength="8">
<label for="confirm-pwd">Confirm Password:</label>
<input type="password" id="confirm-pwd" name="confirmPassword"
required minlength="8">
<button type="submit">Sign Up</button>
</form>
<script>
const pwd = document.getElementById('pwd');
const confirmPwd = document.getElementById('confirm-pwd');
function validatePasswords() {
if (pwd.value !== confirmPwd.value) {
confirmPwd.setCustomValidity('Passwords do not match');
} else {
confirmPwd.setCustomValidity('');
}
}
pwd.addEventListener('input', validatePasswords);
confirmPwd.addEventListener('input', validatePasswords);
</script>
setCustomValidity() sets a custom error message. When the string is non-empty, the field is invalid. Passing an empty string marks it as valid. This is the standard way to implement password confirmation matching.
/* Style invalid fields with a red border */
input:invalid,
textarea:invalid,
select:invalid {
border-color: #ef4444;
}
/* Style valid fields with a green border */
input:valid,
textarea:valid,
select:valid {
border-color: #22c55e;
}
/* Only show validation styles after user interaction */
input:user-invalid {
border-color: #ef4444;
background-color: #fef2f2;
}
input:user-valid {
border-color: #22c55e;
background-color: #f0fdf4;
}
/* Style the validation message */
input:invalid + .error-message {
display: block;
color: #ef4444;
font-size: 0.875rem;
}
The :valid and :invalid pseudo-classes apply immediately on page load, which can be jarring. The newer :user-valid and :user-invalid pseudo-classes only activate after the user has interacted with the field, providing a better UX. Browser support for :user-valid/:user-invalid is growing but not yet universal.
<form id="api-form" novalidate>
<label for="code">Invite Code:</label>
<input type="text" id="code" name="code" required
pattern="[A-Z]{3}-[0-9]{4}"
title="Format: ABC-1234">
<span class="error" id="code-error"></span>
<button type="submit">Verify</button>
</form>
<script>
const form = document.getElementById('api-form');
const codeInput = document.getElementById('code');
const codeError = document.getElementById('code-error');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!codeInput.checkValidity()) {
if (codeInput.validity.valueMissing) {
codeError.textContent = 'Invite code is required.';
} else if (codeInput.validity.patternMismatch) {
codeError.textContent = 'Format must be ABC-1234.';
}
return;
}
codeError.textContent = '';
console.log('Valid code:', codeInput.value);
});
</script>
The novalidate attribute on the form disables the browser's native validation UI, allowing you to implement custom error display. The validity object contains boolean properties like valueMissing, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, and typeMismatch for granular error handling.
required, pattern, minlength, maxlength, Custom Validity, Constraint Validation API