Forms are where people type things wrong. The browser can check many rules for you (a required field, an email shape, a number in range) and JavaScript can read the results through the Constraint Validation API. This lesson covers the HTML rules, the validity object that reports each problem, custom messages, and how to show errors your own way.
You set rules with attributes on form fields. The browser checks them when the form is submitted and stops with a message if a rule is broken:
| Attribute | Rule |
|---|---|
required | the field cannot be empty |
type="email" | must look like an email address (also url, number) |
minlength / maxlength | least and most characters |
min / max | smallest and largest number or date |
step | a number must be a multiple of this (from min) |
pattern | must match a regular expression |
<form>
<input name="code" required pattern="[A-Z]{3}-[0-9]{4}" placeholder="ABC-1234">
<input name="age" type="number" min="13" max="120">
<button>Send</button>
</form>
Every field has a validity object. Each property is true when that specific rule is broken, and validity.valid is true only when nothing is wrong:
| Property | True when |
|---|---|
valueMissing | a required field is empty |
typeMismatch | the value does not fit the type (an email without an @) |
patternMismatch | the value does not match pattern |
rangeUnderflow / rangeOverflow | a number is below min or above max |
tooShort / tooLong | text is under minlength or over maxlength (only after the person edits it) |
stepMismatch | a number does not fit the step |
customError | your own code marked it invalid |
valid | no problems at all |
You can create a field in JavaScript and test the rules without a page. checkValidity() returns true or false:
const input = document.createElement("input");
input.required = true;
console.log(input.validity.valueMissing, input.checkValidity());
input.value = "Ava";
console.log(input.validity.valueMissing, input.checkValidity());
const input = document.createElement("input");
input.type = "email";
input.value = "ava";
console.log(input.validity.typeMismatch);
input.value = "ava@school.org";
console.log(input.validity.typeMismatch, input.validity.valid);
const input = document.createElement("input");
input.type = "number";
input.min = 1;
input.max = 10;
input.value = "0";
console.log(input.validity.rangeUnderflow, input.validity.rangeOverflow);
input.value = "11";
console.log(input.validity.rangeUnderflow, input.validity.rangeOverflow);
input.value = "5";
console.log(input.validity.valid);
const input = document.createElement("input");
input.pattern = "[A-Z]{3}-[0-9]{4}";
for (const value of ["ABC-1234", "abc-1234", "AB-12"]) {
input.value = value;
console.log(value, input.validity.patternMismatch);
}
minlength and maxlength are checked only when the person has edited the field, not when your code sets its value. That is why tooShort is not shown in these examples. It is a quirk of the browser, not a bug in your code.
Some rules HTML cannot express: "the two passwords must match". input.setCustomValidity(message) marks a field invalid with your own message. Clearing it with an empty string is essential: a field stays invalid until you do:
const input = document.createElement("input");
input.value = "abc";
input.setCustomValidity("Please use at least 5 letters");
console.log(input.validity.customError, input.checkValidity());
console.log(input.validationMessage);
input.setCustomValidity("");
console.log(input.validity.customError, input.checkValidity());
form.checkValidity() is true only if every field passes. form.reportValidity() does the same and also shows the browser's message on the first bad field. Inside a submit handler, stop a bad form with event.preventDefault():
form.addEventListener("submit", function (event) {
if (!form.checkValidity()) {
event.preventDefault();
console.log("Fix the form first");
}
});
The built-in tooltips are plain. Add novalidate to the form to switch them off, and write each field's validationMessage into the page yourself. This form checks as you click Send and also checks that the two passwords match:
<form id="f" novalidate>
<p><input id="email" type="email" required placeholder="Email"> <span id="e1" style="color:crimson"></span></p>
<p><input id="pw" type="password" required placeholder="Password"></p>
<p><input id="pw2" type="password" required placeholder="Repeat password"> <span id="e2" style="color:crimson"></span></p>
<button>Sign up</button>
<p id="done"></p>
</form>
<script>
const form = document.getElementById("f");
const email = document.getElementById("email");
const pw = document.getElementById("pw");
const pw2 = document.getElementById("pw2");
form.addEventListener("submit", function (event) {
event.preventDefault();
pw2.setCustomValidity(pw.value === pw2.value ? "" : "Passwords do not match");
document.getElementById("e1").textContent = email.validationMessage;
document.getElementById("e2").textContent = pw2.validationMessage;
document.getElementById("done").textContent = form.checkValidity() ? "Looks good!" : "";
});
</script>
CSS can react to the same rules. :invalid and :valid match fields by their state. A field that is empty and required counts as invalid from the start, which looks harsh, so many sites use :user-invalid, which applies only after the person has interacted with the field:
input:user-invalid { border-color: crimson; }
input:valid { border-color: seagreen; }
Which property is true when a required field is left empty?
validity.typeMismatchvalidity.valueMissingvalidity.customErrorvalidity.tooShortvalueMissing flags an empty required field.Why must you call setCustomValidity("") once the problem is fixed?
What does this log?
const input = document.createElement("input");
input.type = "number";
input.max = 10;
input.value = "11";
console.log(input.validity.rangeOverflow);
true. 11 is above the max of 10.pattern that requires the username to start with a lettermin 13 and show a friendly message when it fails