Skip to content
WANCODE
BM

Form Validation (Constraint Validation API)

Use HTML's built-in rules (`required`, `type`, `pattern`) with your own accessible error messages — no library.

Live preview Open in Playground
index.html
<form id="signup" novalidate>
  <div class="field">
    <label for="name">Name</label>
    <input id="name" name="name" required minlength="2"
           autocomplete="name" aria-describedby="name-error">
    <p class="error" id="name-error"></p>
  </div>

  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required
           autocomplete="email" aria-describedby="email-error">
    <p class="error" id="email-error"></p>
  </div>

  <div class="field">
    <label for="postcode">Postcode</label>
    <input id="postcode" name="postcode" required inputmode="numeric"
           pattern="[0-9]{5}" data-pattern-message="Malaysian postcodes have 5 digits."
           aria-describedby="postcode-error">
    <p class="error" id="postcode-error"></p>
  </div>

  <button type="submit">Sign up</button>
  <p role="status" id="form-status"></p>
</form>
validate.js
const form = document.getElementById("signup");
const statusEl = document.getElementById("form-status");

function messageFor(input) {
  const v = input.validity;
  if (v.valueMissing) return "Please fill in this field.";
  if (v.typeMismatch) return "Please enter a valid " + input.type + " address.";
  if (v.tooShort) return "Use at least " + input.minLength + " characters (you typed " + input.value.length + ").";
  if (v.patternMismatch) return input.dataset.patternMessage || "Please match the requested format.";
  return input.validationMessage; // anything else: the browser's own message
}

function showError(input) {
  const errorEl = document.getElementById(input.id + "-error");
  const valid = input.validity.valid;
  input.setAttribute("aria-invalid", String(!valid));
  if (errorEl) errorEl.textContent = valid ? "" : messageFor(input);
  return valid;
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const fields = Array.from(form.querySelectorAll("input, select, textarea"));
  const firstInvalid = fields.filter((field) => !showError(field))[0];

  if (firstInvalid) {
    firstInvalid.focus();
    statusEl.textContent = "";
    return;
  }

  const data = Object.fromEntries(new FormData(form));
  statusEl.textContent = "Thanks, " + data.name + "! (Nothing was sent — this is a demo.)";
  console.log("Valid data:", data);
});

// Re-check a field as the user fixes it — but only after it was marked invalid
form.addEventListener("input", (event) => {
  if (event.target.getAttribute("aria-invalid") === "true") showError(event.target);
});

HTML already knows how to validate: required, type="email", minlength, pattern. The Constraint Validation API lets JavaScript read the result (input.validity) so you can show clear messages in your own design, next to each field.

Line by line

  • novalidate on the form turns off the browser's pop-up bubbles but keeps the rules — validity still works. We take over the display.
  • input.validity is a ValidityState object with flags like valueMissing, typeMismatch, tooShort and patternMismatch. Each flag maps to one HTML attribute, so each gets its own message.
  • pattern="[0-9]{5}" must match the whole value (it's anchored automatically). data-pattern-message keeps the human message next to the rule in the HTML.
  • aria-invalid="true" tells screen readers the field has a problem, and aria-describedby links the field to its error text so it's read out when the field is focused.
  • On submit we validate everything, then move focus to the first invalid field so keyboard and screen-reader users land right where the problem is.
  • The input listener only re-checks fields that are already marked invalid, so people aren't told off while they're still typing for the first time.

Client-side isn't security

Browser validation is for a good experience. Anyone can bypass it, so the server must validate the same data again.

Usage

HTML
<!-- Any input with an id, a matching "<id>-error" element and
     aria-describedby pointing at it gets custom messages. -->
<input id="phone" name="phone" type="tel" required
       pattern="01[0-9]-?[0-9]{7,8}"
       data-pattern-message="Use a Malaysian mobile number, e.g. 012-3456789."
       aria-describedby="phone-error">
<p class="error" id="phone-error"></p>