Client-side form validation

Client-side validation is an initial check and an important feature of good user experience; by catching invalid data on the client-side, the user can fix it straight away. If it gets to the server and is then rejected, a noticeable delay is caused by a round trip to the server and then back to the client-side to tell the user to fix their data.

However, client-side validation should not be considered an exhaustive security measure! Your apps should always perform validation, including security checks, on any form-submitted data on the server-side as well as the client-side, because client-side validation is too easy to bypass, so malicious users can still easily send bad data through to your server.

Different types of client-side validation

There are two different types of client-side validation that you'll encounter on the web:

  • HTML form validation HTML form attributes can define which form controls are required and which format the user-entered data must be in to be valid.
  • JavaScript form validation JavaScript is generally included to enhance or customize HTML form validation.

Client side validation can be accomplished with little to no JavaScript. HTML validation is faster than JavaScript, but is less customizable than JavaScript validation. It is generally recommended to begin your forms using robust HTML features, then enhance the user experience with JavaScript as needed.

Using built-in form validation

One of the most significant features of form controls is the ability to validate most user data without relying on JavaScript. This is done by using validation attributes on form elements. We've seen many of these earlier in the course, but to recap:

  • required: Specifies whether a form field needs to be filled in before the form can be submitted.
  • minlength and maxlength: Specifies the minimum and maximum length of textual data (strings).
  • min, max, and step: Specifies the minimum and maximum values of numerical input types, and the increment, or step, for values, starting from the minimum.
  • type: Specifies whether the data needs to be a number, an email address, or some other specific preset type.
  • pattern: Specifies a regular expression that defines a pattern the entered data needs to follow.

If the data entered in a form field follows all of the rules specified by the attributes applied to the field, it is considered valid. If not, it is considered invalid.

When an element is valid, the following things are true:

  • The element matches the :valid CSS pseudo-class, which lets you apply a specific style to valid elements. The control will also match :user-valid if the user has interacted with the control, and may match other UI pseudo-classes, such as :in-range, depending on the input type and attributes.
  • If the user tries to send the data, the browser will submit the form, provided there is nothing else stopping it from doing so (e.g., JavaScript).

When an element is invalid, the following things are true:

  • The element matches the :invalid CSS pseudo-class. If the user has interacted with the control, it also matches the :user-invalid CSS pseudo-class. Other UI pseudo-classes may also match, such as :out-of-range, depending on the error. These let you apply a specific style to invalid elements.
  • If the user tries to send the data, the browser will block the form submission and display an error message. The error message will differ depending on the type of error. The Constraint Validation API is described below.

Built-in form validation examples

In this section, we'll test out some of the attributes that we discussed above.

Simple start file

Let's start with a simple example: an input that allows you to choose whether you prefer a banana or a cherry. This example involves a basic text <input> with an associated <label> and a submit <button>.

<form>
  <label for="choose">Would you prefer a banana or cherry?</label>
  <input id="choose" name="i-like" />
  <button>Submit</button>
</form>
input:invalid {
  border: 2px dashed red;
}
 
input:valid {
  border: 2px solid black;
}

The required attribute

A common HTML validation feature is the required attribute. Add this attribute to an input to make an element mandatory. When this attribute is set, the element matches the :required UI pseudo-class and the form won't submit, displaying an error message on submission, if the input is empty. While empty, the input will also be considered invalid, matching the :invalid UI pseudo-class.

If any radio button in a same-named group has the required attribute, one of the radio buttons in that group must be checked for the group to be valid; the checked radio doesn't have to be the one with the attribute set.

Validating against a regular expression

Another useful validation feature is the pattern attribute, which expects a Regular Expression as its value. A regular expression (regexp) is a pattern that can be used to match character combinations in text strings, so regexps are ideal for form validation and serve a variety of other uses in JavaScript.

Regexps are quite complex, and we don't intend to teach you them exhaustively in this article. Below are some examples to give you a basic idea of how they work.

  • a — Matches one character that is a (not b, not aa, and so on).
  • abc — Matches a, followed by b, followed by c.
  • ab?c — Matches a, optionally followed by a single b, followed by c. (ac or abc) ab*c — Matches a, optionally followed by any number of bs, followed by c. (ac, abc, abbbbbc, and so on). abc|xyz — Matches exactly abc or exactly xyz (but not abcxyz or a or y, and so on).

Validating forms using JavaScript

If you want to change the text of the native error messages, JavaScript is needed. In this section we will look at the different ways to do this.

The Constraint Validation API

The Constraint Validation API consists of a set of methods and properties available on the following form element DOM interfaces:

HTMLButtonElement, HTMLFieldSetElement, HTMLInputElement, HTMLOutputElement, HTMLSelectElement, HTMLTextAreaElement

The Constraint Validation API makes the following properties available on the above elements.

  • validationMessage: Returns a localized message describing the validation constraints that the control doesn't satisfy (if any). If the control is not a candidate for constraint validation (willValidate is false) or the element's value satisfies its constraints (is valid), this will return an empty string.
  • validity: Returns a ValidityState object that contains several properties describing the validity state of the element. You can find full details of all the available properties in the ValidityState reference page; below is listed a few of the more common ones:
    • valid: Returns true if the element meets all its validation constraints, and is therefore considered to be valid, or false if it fails any constraint. If true, the element matches the :valid CSS pseudo-class; the :invalid CSS pseudo-class otherwise.
    • valueMissing: Returns true if the element has a required attribute, but no value, or false otherwise. If true, the element matches the :invalid CSS pseudo-class.
    • typeMismatch: Returns true if the value is not in the required syntax (when type is email or url), or false if the syntax is correct. If true, the element matches the :invalid CSS pseudo-class.
    • typeMismatch: Returns true if the value is not in the required syntax (when type is email or url), or false if the syntax is correct. If true, the element matches the :invalid CSS pseudo-class.

Implementing a customized error message

As you saw in the HTML validation constraint examples earlier, each time a user tries to submit an invalid form, the browser displays an error message. The way this message is displayed depends on the browser.

Customizing these error messages is one of the most common use cases of the Constraint Validation API. Let's work through an example of how to do this.

<form>
  <label for="mail">
    I would like you to provide me with an email address:
  </label>
  <input type="email" id="mail" name="mail" />
  <button>Submit</button>
</form>

Add the following JavaScript to the page:

const email = document.getElementById("mail");
 
email.addEventListener("input", (event) => {
  if (email.validity.typeMismatch) {
    email.setCustomValidity("I am expecting an email address!");
  } else {
    email.setCustomValidity("");
  }
});

Here we store a reference to the email input, then add an event listener to it that runs the contained code each time the value inside the input is changed.

Inside the contained code, we check whether the email input's validity.typeMismatch property returns true, meaning that the contained value doesn't match the pattern for a well-formed email address. If so, we call the setCustomValidity() method with a custom message. This renders the input invalid, so that when you try to submit the form, submission fails and the custom error message is displayed.

If the validity.typeMismatch property returns false, we call the setCustomValidity() method with an empty string. This renders the input valid, so the form will submit. During validation, if any form control has a customError that is not the empty string, form submission is blocked.

Example

<form novalidate>
  <p>
    <label for="mail">
      <span>Please enter an email address:</span>
      <input type="email" id="mail" name="mail" required minlength="8" />
      <span class="error" aria-live="polite"></span>
    </label>
  </p>
  <button>Submit</button>
</form>

This form uses the novalidate attribute to turn off the browser's automatic validation. Setting the novalidate attribute on the form stops the form from showing its own error message bubbles, and allows us to instead display the custom error messages in the DOM in some manner of our own choosing. However, this doesn't disable support for the constraint validation API nor the application of CSS pseudo-classes like :valid, etc. That means that even though the browser doesn't automatically check the validity of the form before sending its data, you can still do it yourself and style the form accordingly.

Our input to validate is an <input type="email">, which is required, and has a minlength of 8 characters. Let's check these using our own code, and show a custom error message for each one.

We are aiming to show the error messages inside a <span> element. The aria-live attribute is set on that <span> to make sure that our custom error message will be presented to everyone, including it being read out to screen reader users.

Now onto some basic CSS to improve the look of the form slightly, and provide some visual feedback when the input data is invalid:

body {
  font: 1em sans-serif;
  width: 200px;
  padding: 0;
  margin: 0 auto;
}
 
p * {
  display: block;
}
 
input[type="email"] {
  appearance: none;
 
  width: 100%;
  border: 1px solid #333;
  margin: 0;
 
  font-family: inherit;
  font-size: 90%;
 
  box-sizing: border-box;
}
 
/* invalid fields */
input:invalid {
  border-color: #900;
  background-color: #fdd;
}
 
input:focus:invalid {
  outline: none;
}
 
/* error message styles */
.error {
  width: 100%;
  padding: 0;
 
  font-size: 80%;
  color: white;
  background-color: #900;
  border-radius: 0 0 5px 5px;
 
  box-sizing: border-box;
}
 
.error.active {
  padding: 0.3em;
}

Now let's look at the JavaScript that implements the custom error validation. There are many ways to pick a DOM node; here we get the form itself and the email input box, as well as the span element into which we will place the error message.

Using event handlers, we check if the form fields are valid each time the user types something. If there is an error, we show it. If there is no error, we remove any error messaging.

const form = document.querySelector("form");
const email = document.getElementById("mail");
const emailError = document.querySelector("#mail + span.error");
 
email.addEventListener("input", (event) => {
  if (email.validity.valid) {
    emailError.textContent = ""; // Remove the message content
    emailError.className = "error"; // Removes the `active` class
  } else {
    // If there is still an error, show the correct error
    showError();
  }
});
 
form.addEventListener("submit", (event) => {
  // if the email field is invalid
  if (!email.validity.valid) {
    // display an appropriate error message
    showError();
    // prevent form submission
    event.preventDefault();
  }
});
 
function showError() {
  if (email.validity.valueMissing) {
    // If empty
    emailError.textContent = "You need to enter an email address.";
  } else if (email.validity.typeMismatch) {
    // If it's not an email address,
    emailError.textContent = "Entered value needs to be an email address.";
  } else if (email.validity.tooShort) {
    // If the value is too short,
    emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`;
  }
  // Add the `active` class
  emailError.className = "error active";
}

Every time we change the value of the input, we check to see if it contains valid data. If it has then we remove any error message being shown. If the data is not valid, we run showError() to show the appropriate error.

Every time we try to submit the form, we again check to see if the data is valid. If so, we let the form submit. If not, we run showError() to show the appropriate error, and stop the form submitting with preventDefault().