The Constraint Validation API
HTML's built-in validation attributes (required, pattern, and friends)
are convenient, but their error messages are ugly, unstylable, and hard to
customize. The Constraint Validation API is the JavaScript layer that
sits on top of those same attributes — it lets you check validity
programmatically, show your own custom error UI, and combine native rules
with validation logic HTML alone can't express.
checkValidity()
Every form-associated element (and the <form> itself) has a
checkValidity() method. It returns true/false and, if the element is
invalid, fires an invalid event — but it does not show the native
error bubble.
check-validity.html
<input type="email" id="email" required />
<script>
const input = document.getElementById('email');
console.log(input.checkValidity()); // true or false
</script>Calling form.checkValidity() checks every field in the form at once and
returns true only if all of them pass.
reportValidity()
reportValidity() does everything checkValidity() does, and — if the
element is invalid — also shows the browser's native error bubble and
focuses the field, exactly like a failed form submission would.
report-validity.html
<form novalidate id="my-form">
<input type="email" id="email" required />
<button type="button" id="check-btn">Check</button>
</form>
<script>
document.getElementById('check-btn').addEventListener('click', () => {
const form = document.getElementById('my-form');
if (form.reportValidity()) {
console.log('All fields are valid!');
}
});
</script>novalidate on the <form> disables the browser's automatic validation on submit, letting you call reportValidity() yourself — for example, after also running some custom async check (like "is this username already taken?").The ValidityState Object
Every validated element exposes .validity, a ValidityState object with
a set of boolean properties describing why it's invalid — far more
specific than a single true/false.
Property | True when… |
|---|---|
| A |
| Value doesn't match the input's |
| Value fails the |
|
|
|
|
| Value isn't a valid |
| A custom message was set via |
| True only when none of the above are true |
validity-state.html
<input type="email" id="email" required />
<script>
const input = document.getElementById('email');
input.addEventListener('invalid', () => {
const { valueMissing, typeMismatch } = input.validity;
if (valueMissing) console.log('Email is required.');
else if (typeMismatch) console.log("That doesn't look like a valid email.");
});
</script>setCustomValidity() — Custom Rules
setCustomValidity(message) lets you mark a field invalid for a reason
HTML's built-in attributes can't express — like "these two passwords don't
match." Passing a non-empty string marks the field invalid and sets that
string as the browser's error message; passing an empty string clears the
custom error and lets normal validation resume.
custom-validity.html
<form novalidate>
<label for="password">Password</label>
<input type="password" id="password" required minlength="8" />
<label for="confirm">Confirm password</label>
<input type="password" id="confirm" required />
<button type="submit">Create account</button>
</form>
<script>
const password = document.getElementById('password');
const confirm = document.getElementById('confirm');
function validateMatch() {
if (confirm.value !== password.value) {
confirm.setCustomValidity('Passwords do not match.');
} else {
confirm.setCustomValidity('');
}
}
password.addEventListener('input', validateMatch);
confirm.addEventListener('input', validateMatch);
</script>setCustomValidity() sets a non-empty message, the field stays invalid **forever** — even if the user fixes it — until your code explicitly calls setCustomValidity('') again. Forgetting the "clear" branch is the most common bug with this API.Combining Native and Custom Validation
The real power of this API is layering custom JavaScript logic (async checks, cross-field rules, business logic) on top of the free native checks HTML attributes already give you — instead of reimplementing required-field or pattern checks from scratch.
combined-validation.html
<form novalidate id="username-form">
<label for="username">Username</label>
<input
type="text"
id="username"
pattern="[a-zA-Z0-9_]{3,16}"
required
/>
<span id="username-error" role="alert"></span>
<button type="submit">Check availability</button>
</form>
<script>
const form = document.getElementById('username-form');
const username = document.getElementById('username');
const errorEl = document.getElementById('username-error');
form.addEventListener('submit', async (event) => {
event.preventDefault();
errorEl.textContent = '';
// Native checks (required, pattern) first
if (!username.checkValidity()) {
errorEl.textContent = username.validationMessage;
username.reportValidity();
return;
}
// Then a custom async check
const taken = await isUsernameTaken(username.value);
if (taken) {
username.setCustomValidity('That username is already taken.');
username.reportValidity();
username.setCustomValidity(''); // clear so it revalidates next time
return;
}
form.submit();
});
</script>validationMessage
element.validationMessage returns the human-readable string the browser
would show natively for the current validity state — handy for building
your own custom error UI without hand-writing every message yourself.
checkValidity()— silently checks, firesinvalidevent, returns boolean.reportValidity()— checks AND shows the native UI/focuses the field.validity— aValidityStatewith specific true/false reasons.validationMessage— the human-readable message for the current state.setCustomValidity(msg)— force a custom invalid state (remember to clear it).