Validation and Autofill

Constraint Validation and Autofill

Browsers validate forms before they send them. You declare the rules as attributes, the browser checks them when the user submits, focuses the first invalid control, shows a message in the user's language, and cancels the submission. This constraint validation costs no JavaScript, and a small API lets you customize every step. It never replaces server-side validation: anyone can edit the HTML in DevTools or send a request with curl 3,008 .

Declarative constraints and the flags they set
Constraint Applies to ValidityState flag
required Most controls valueMissing
type="email", type="url" Those types typeMismatch
pattern="[A-Z]{3}-\d{4}" Text-like types patternMismatch
min, max Numbers, ranges, dates rangeUnderflow, rangeOverflow
step Numbers, ranges, dates stepMismatch
minlength, maxlength Text types, <textarea> tooShort, tooLong
Unparseable input, e.g. letters typed in a date Date, number badInput
setCustomValidity("message") Any control customError

pattern must match the whole value, as if wrapped in ^(?:...)$, and it is compiled with the regular expression v flag, so characters such as -, ( and | need escaping inside a character class. Disabled and readonly controls, hidden inputs and type="button" or "reset" inputs are never validated. Add novalidate to the form, or formnovalidate to one submit button, to switch validation off.

The Constraint Validation API

Every control exposes validity (a ValidityState with the flags above plus valid), validationMessage, willValidate, checkValidity(), reportValidity() and setCustomValidity(); the form has the two check methods too. checkValidity() returns a boolean and fires an invalid event on each failing control; reportValidity() also shows the browser's message. A non-empty custom message keeps the control invalid until you set it back to "".

Inspecting validity states and custom errorsJavaScript
const form = document.createElement('form');
form.innerHTML = `<input name="email" type="email" required>
  <input name="age" type="number" min="18">
  <input name="code" pattern="[A-Z]{3}-\\d{4}"><input name="pw2" type="password">`;
const { email, age, code, pw2 } = form.elements;
[email.value, age.value, code.value] = ['ana@', '17.5', 'abc-12'];
pw2.setCustomValidity('Passwords do not match.');   // e.g. after comparing with the first field
const flags = (el) => Object.keys(ValidityState.prototype).filter((k) => el.validity[k]);
for (const el of [email, age, code, pw2]) {
  console.log(`${el.name}: [${flags(el)}] ${el.validationMessage}`);
}
console.log('form valid:', form.checkValidity());
Output
email: [typeMismatch] Please enter a part following '@'. 'ana@' is incomplete.
age: [rangeUnderflow,stepMismatch] Value must be greater than or equal to 18.
code: [patternMismatch] Please match the requested format.
pw2: [customError] Passwords do not match.
form valid: false

The messages come from Chrome 1 and differ in other browsers and languages; for a pattern mismatch the bubble may also show the title text. Styling follows the same state: :invalid matches immediately, which paints an empty required field red before the user has typed anything, while :user-invalid waits until the user has changed the field and left it, or tried to submit (Baseline since November 2023; Input Selectors covers these selectors).

Inline error messages with :user-invalid and the invalid eventHTMLLive
<style>
  body { font: 15px/1.4 system-ui, sans-serif; margin: 12px; }
  input { font: inherit; display: block; width: 260px; border: 1px solid #889; padding: 3px; }
  input:user-invalid { border: 2px solid #c62828; background: #ffebee; }
  input:user-valid { border: 2px solid #2e7d32; }
  label, .err { display: block; margin-bottom: 4px; }
  .err { color: #c62828; font-size: 13px; }
</style>
<form id="signup">
  <label>Email <input name="email" type="email" required autocomplete="username">
    <span class="err"></span></label>
  <label>Password (8+ characters)
    <input name="pw" type="password" minlength="8" required autocomplete="new-password">
    <span class="err"></span></label>
  <button>Create account</button>
</form>
<script>
  const form = document.getElementById('signup');
  form.addEventListener('invalid', (e) => {       // invalid does not bubble: listen in capture
    e.preventDefault();                           // suppress the browser bubble
    e.target.nextElementSibling.textContent = e.target.validationMessage;
  }, true);
  form.addEventListener('input', (e) => {
    e.target.setCustomValidity('');
    e.target.nextElementSibling.textContent = '';
  });
  form.elements.email.value = 'ana@example.com';
  form.elements.pw.value = 'short';               // minlength only checks user edits...
  form.elements.pw.setCustomValidity('Use at least 8 characters.');   // ...so flag it here
  form.requestSubmit();                           // runs validation like a click on the button
</script>
Browser output of Listing 2.31
Browser output of 31

minlength and maxlength check only values the user typed, not values set by script, which is why the demo sets a custom error. requestSubmit() validates and fires submit; form.submit() skips both.

Autofill

The autocomplete attribute tells browsers and password managers what a field holds, so they can fill a whole address or card in one tap. Beyond on and off it takes standardized tokens such as name, email, tel, street-address, postal-code, cc-number, username, current-password, new-password and one-time-code. Prefixes scope a token: shipping postal-code, work tel, or section-guest1 email when one page holds two people's details. webauthn goes last, as in autocomplete="username webauthn": when the page calls navigator.credentials.get() with mediation: 'conditional', saved passkeys appear among the suggestions.

Practical Advice