Constraint Validation API
Use the browser's built-in Constraint Validation API to read validity state, set custom error messages, and build accessible form validation UIs.
Overview
Key Points
- •`input.validity` is a `ValidityState` object with boolean properties for each constraint type
- •`input.validity.valid` is true only when ALL constraints pass
- •`input.setCustomValidity(msg)` non-empty string marks as invalid; empty string clears the error
- •`input.checkValidity()` returns boolean, fires `invalid` event if invalid
- •`form.checkValidity()` checks ALL controls and fires `invalid` on each invalid one
- •`input.reportValidity()` same as checkValidity but also shows native browser error UI
- •`input.validationMessage` the human-readable error string the browser would show
Examples
Reading ValidityState to show precise error messages
<form id="signup" novalidate>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<span class="error" id="email-error"></span>
</div>
<button type="submit">Sign Up</button>
</form>
<script>
const form = document.getElementById('signup');
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
function validateEmail() {
const v = emailInput.validity;
if (v.valid) {
emailError.textContent = '';
emailInput.removeAttribute('aria-invalid');
return;
}
let msg;
if (v.valueMissing) msg = 'Email is required.';
else if (v.typeMismatch) msg = 'Please enter a valid email address.';
else msg = emailInput.validationMessage;
emailError.textContent = msg;
emailInput.setAttribute('aria-invalid', 'true');
emailInput.setAttribute('aria-describedby', 'email-error');
}
emailInput.addEventListener('blur', validateEmail);
form.addEventListener('submit', (e) => {
e.preventDefault();
validateEmail();
if (form.checkValidity()) {
console.log('Form is valid, submitting...');
}
});
</script>novalidate disables the browser's default tooltip UI so we can show our own styled errors.
Custom validation with setCustomValidity
<form id="password-form" novalidate>
<label>
Password
<input id="pwd" type="password" required minlength="8">
</label>
<label>
Confirm Password
<input id="pwd-confirm" type="password" required>
</label>
<span id="confirm-error"></span>
<button type="submit">Set Password</button>
</form>
<script>
const pwd = document.getElementById('pwd');
const confirm = document.getElementById('pwd-confirm');
const confirmError = document.getElementById('confirm-error');
function validateMatch() {
if (confirm.value !== pwd.value) {
confirm.setCustomValidity('Passwords do not match.');
} else {
confirm.setCustomValidity(''); // clear the error
}
confirmError.textContent = confirm.validationMessage;
}
confirm.addEventListener('input', validateMatch);
pwd.addEventListener('input', validateMatch);
</script>setCustomValidity("") MUST be called to clear a custom error any non-empty string keeps the field invalid.
Programmatic validation without the native UI
<form id="multi-step-form">
<input name="step1-name" required id="s1-name">
<input name="step1-email" type="email" required id="s1-email">
<button type="button" id="next-btn">Next Step</button>
</form>
<script>
document.getElementById('next-btn').addEventListener('click', () => {
const form = document.getElementById('multi-step-form');
// checkValidity fires 'invalid' event on bad fields listen to show custom UI
form.addEventListener('invalid', (e) => {
e.preventDefault(); // stop browser tooltip
e.target.classList.add('field-error');
}, { capture: true, once: false });
if (form.checkValidity()) {
// proceed to next step
showStep(2);
}
});
</script>Listening to the "invalid" event at the form level catches all field failures in one handler.
Common Mistakes
- Calling `setCustomValidity("error")` but forgetting to call `setCustomValidity("")` when the error is resolved the field stays permanently invalid
- Using `novalidate` on a form without adding your own validation users can submit empty required fields
- Reading `input.validationMessage` as the source of truth for which constraint failed use `validity.valueMissing` etc. for precise branching
Note
The `invalid` event does NOT bubble attach listeners to the form using capture phase (`{capture: true}`) to handle all invalid fields in one place rather than individual per-field listeners.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4