HTMLConstraint Validation API

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

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

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 + reportValidity() is a common pairing
Putting 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…

valueMissing

A required field has no value

typeMismatch

Value doesn't match the input's type (e.g. bad email/url format)

patternMismatch

Value fails the pattern regex

tooShort / tooLong

minlength / maxlength violated

rangeUnderflow / rangeOverflow

min / max violated

stepMismatch

Value isn't a valid step increment

customError

A custom message was set via setCustomValidity()

valid

True only when none of the above are true

validity-state.html

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

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>
Always clear it when the value becomes valid
Once 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

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, fires invalid event, returns boolean.

  • reportValidity() — checks AND shows the native UI/focuses the field.

  • validity — a ValidityState with specific true/false reasons.

  • validationMessage — the human-readable message for the current state.

  • setCustomValidity(msg) — force a custom invalid state (remember to clear it).

Great for cross-field and async rules
Reach for this API whenever a rule can't be expressed by a single input's attributes alone: password confirmation, "end date after start date," server-side uniqueness checks, or business rules tied to other form state.
Still pair with server-side validation
Just like plain HTML validation attributes, everything here runs in the browser and can be bypassed. Treat it as UX polish, and always re-validate every field again on the server.