Validation

Let the browser enforce the rules, write a useful message when it cannot, and know why the server still checks.

13of 22
≈ 12Min read
8Parts

The browser can do most of it

Constraint validation is built in. You describe the rule in an attribute; the browser blocks the submission, focuses the first bad field and announces the problem in the user's own language. No JavaScript, no library.

AttributeRule
requiredMust not be empty.
minlength / maxlengthLength of text. maxlength stops typing; minlength only blocks submission.
min / maxRange for numbers, dates and times.
stepAllowed increments. step="0.01" for money, step="any" to switch it off.
patternA regular expression the whole value must match.
typeemail and url validate their own format.

pattern needs a title

pattern is anchored at both ends automatically — you do not write ^ or $. Always pair it with title describing the format, because the browser puts that text in the error message.

a four-digit PIN
<label for="pin">PIN</label>
<input type="text" id="pin" name="pin"
       inputmode="numeric"
       pattern="[0-9]{4}"
       title="Four digits"
       required>
Do not invent an email pattern

Valid email addresses are far stranger than the regular expressions people write for them. Use type="email" and, if it matters, send a confirmation message — that is the only real test.

Styling the states

CSS can see validity. The trick is to wait until the reader has had a chance: :user-invalid only matches after they have interacted with the field, so a blank form is not covered in red before anyone types.

  • :required and :optional — what the field is.
  • :valid / :invalid — match immediately, including on an untouched empty field.
  • :user-valid / :user-invalid — match only after interaction. Prefer these.
  • :in-range / :out-of-range — for numbers with min and max.
  • :placeholder-shown — useful for floating-label effects.
restrained feedback
input:user-invalid {
  border-color: #c2452d;
}

input:user-valid {
  border-color: #3f8f5e;
}

/* the whole form, only once submission has been attempted */
form.submitted input:invalid {
  border-color: #c2452d;
}

The API

reading validity
const email = document.querySelector("#email");

email.validity.valid;         // true or false
email.validity.valueMissing;  // required, but empty
email.validity.typeMismatch;  // not an email address
email.validity.patternMismatch;
email.validity.tooShort;
email.validity.rangeOverflow;

email.validationMessage;      // the browser's own wording
form.checkValidity();         // true/false, fires invalid events
form.reportValidity();        // the same, and shows the bubbles

Your own message

setCustomValidity replaces the message for a field. A non-empty string makes the field invalid; an empty string clears it again. Clearing it is the step everybody forgets, and the field then stays invalid forever.

confirm a password
const pass = document.querySelector("#password");
const again = document.querySelector("#confirm");

function compare() {
  again.setCustomValidity(again.value === pass.value ? "" : "The two passwords do not match.");
}

pass.addEventListener("input", compare);
again.addEventListener("input", compare);

Replacing the bubbles

The native bubbles cannot be styled. To use your own messages, stop the default with novalidate on the form — the API keeps working, only the browser's UI is suppressed — and write the message into an element the field points at.

a message a screen reader will read
<form novalidate>
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required
         aria-describedby="email-error">
  <p id="email-error" class="error" role="alert"></p>
  <button>Send</button>
</form>
script.js
const form = document.querySelector("form");
const email = form.email;
const out = document.getElementById("email-error");

form.addEventListener("submit", (e) => {
  out.textContent = "";
  if (!email.checkValidity()) {
    e.preventDefault();
    out.textContent = email.validity.valueMissing
      ? "Please enter your email address."
      : "That does not look like an email address.";
    email.focus();
  }
});

Turning it off, on purpose

  • novalidate on the form — suppress the browser UI, keep the API.
  • formnovalidate on a button — this one button submits without validating. Exactly right for "Save draft".

The rule that matters most

Client-side validation is a convenience. It saves a round trip and gives instant feedback. It is not a defence: anyone can send a request without your page, using the browser console, curl or a script.

Every rule you enforce in the browser must be enforced again on the server. Treat the browser's version as a nicety and the server's version as the truth.

And validate for the right reason

Over-strict rules reject real people: names with apostrophes, addresses abroad, phone numbers with a country code. Ask what actually breaks if the value is odd, and only reject that.

Practice