🚀AdvancedAdvanced Forms

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

Every form control in HTML has a built-in `ValidityState` object that tracks exactly which constraint is violated: `valueMissing`, `typeMismatch`, `patternMismatch`, `tooShort`, `tooLong`, `rangeUnderflow`, `rangeOverflow`, `stepMismatch`, and `customError`. You can read this state programmatically to show tailored error messages rather than relying on the browser's default tooltip UI. The `setCustomValidity(message)` method marks a field as invalid with a custom message. When the message is an empty string, the field is cleared of its custom error. `reportValidity()` triggers the browser's native validation UI on demand. `checkValidity()` silently returns whether the field (or form) is valid without showing any UI. This API enables you to build pixel-perfect validation UIs that remain accessible the validity state is exposed to assistive technology through the browser's underlying form model.

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.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What does input.validity.valid being false guarantee?