Reader Stacks

Validating Matching Passwords With Vanilla JavaScript

The native Constraint Validation API — setCustomValidity() specifically — lets a browser show its own built-in validation message for a custom check like password matching, without a framework.

Outside a framework, checking that a "confirm password" field matches the original password doesn't need a hand-rolled error-message div and manual show/hide logic — the browser's built-in Constraint Validation API can display its own native validation UI for a custom check, the same way it does for required or type="email".

1. The basic HTML

<form id="signup-form">
  <input type="password" id="password" required>
  <input type="password" id="confirm-password" required>
  <button type="submit">Sign up</button>
</form>

2. Using setCustomValidity() for the native validation message

const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm-password');

function checkPasswordsMatch() {
  if (confirmPassword.value !== password.value) {
    confirmPassword.setCustomValidity('Passwords do not match');
  } else {
    confirmPassword.setCustomValidity(''); // clears the custom error — required for the form to submit
  }
}

password.addEventListener('input', checkPasswordsMatch);
confirmPassword.addEventListener('input', checkPasswordsMatch);

setCustomValidity() plugs a custom message into the browser's own native validation system — once set to a non-empty string, the field is considered invalid, and the browser shows its normal built-in validation bubble with that message when the form is submitted, exactly as it would for a native required failure. Setting it back to an empty string is what marks the field valid again; forgetting that step leaves the field permanently blocked from submitting even after the passwords actually match.

3. Why this is often better than a hand-rolled error message

The Constraint Validation API's built-in message bubble is automatically positioned near the field, respects the user's browser and OS accessibility settings, and is announced correctly by screen readers without any additional ARIA work — a custom-built error <div> toggled with JavaScript has to replicate all of that manually to reach the same level of accessibility, and often doesn't.

4. Preventing the invalid state from blocking submission unexpectedly

document.getElementById('signup-form').addEventListener('submit', (e) => {
  checkPasswordsMatch(); // ensure the check has run at least once before submit
  if (!confirmPassword.checkValidity()) {
    e.preventDefault();
  }
});

checkValidity() triggers the browser's native validation check (including any custom validity message set) and returns whether the field currently passes — checking it explicitly on submit, in addition to the live input listeners, guards against a submission attempt before the user has typed into the confirm field at all.

5. Styling the invalid state

input:invalid {
  border-color: #d33;
}
input:valid {
  border-color: #2a2;
}

The native :invalid and :valid CSS pseudo-classes automatically reflect a field's current constraint-validation state, including any custom validity set via setCustomValidity() — this means visual feedback (a red border, for instance) can be added with plain CSS, no JavaScript class-toggling required, since the browser already tracks and exposes that state itself.

6. A note on where the real security check belongs

This client-side check exists purely for immediate user feedback — the actual password confirmation logic still needs to be enforced server-side (matching any backend validation the framework provides), since client-side JavaScript validation can always be bypassed by disabling it, submitting the form directly, or calling the API without a browser at all.

Topics: Forms & Validation