Form validation

  • html
  • forms
  • validation

In this workshop you’ll learn how to validate user input in the browser and present error messages accessibly.

Final solution

Setup

  1. Download starter files
  2. Open workshop/index.html in your browser
  3. This is the form we’ll be adding validation to

Why validate in the browser?

Client-side validation is important for a good user experience—you can quickly give the user feedback when they need to change a value they’ve entered. For example if passwords must be a certain length you can tell them immediately, rather than waiting for the form to submit to the server and receive an invalid response.

You should not rely entirely on client-side validation however. You can never trust anything that happens in the browser: users can use dev tools to mess with attributes and elements to bypass validation. You always need to validate user input on the server as well, for security.

Communicating requirements

Our form has two inputs: one for an email address and one for a password. These are the requirements we need to validate:

  1. Both values are present
  2. The email value is a valid email address
  3. The password contains at least one number, and is at least 8 characters long

Before we implement validation we need to make sure the user is aware of the requirements, by labelling the inputs. There’s nothing more frustrating than trying to guess what you need to do to be able to submit a form.

Required values

Users generally expect required fields to be marked with an asterisk (*). We can add one inside the <label>. However this will cause screen readers to read out the label as “email star”, which is not correct. We should wrap the asterisk in an element with aria-hidden="true" to ensure it is ignored by assistive technology.

Different types of value

If we don’t list our password requirements users will have to guess what they are.

The simplest way to list requirements is in a <div> following the label. This is fine for visual users but won’t be linked to the input, which means assistive tech will ignore it.

We need to use the aria-describedby attribute on the input. This takes the IDs of other elements that provide additional info. It allows us to link the div to the input so screen readers read out the extra info as if it were part of the label.

Challenge

  1. Add a visual required indicator to both inputs.
  2. Add instructions containing our password requirements
  3. Associate the instructions with the input using aria-describedby
Toggle answer
<label for="password">
Password
<span aria-hidden="true">*</span>
</label>
<div id="passwordRequirements">
Passwords must contain at least one letter and one number, and contain at
least 8 characters.
</div>
<input id="password" aria-describedby="passwordRequirements" />

If you inspect the password input in Chrome’s devtools you should be able to see the accessible name (from the label) and description (from the div) in the “Accessibility tab”.

Accessibility tab example

HTML5 validation

Now we need to tell the user when they enter invalid values. Browsers support lots of different types of validation.

Requiring values

The required attribute will stop the user submitting the form if they haven’t entered this value yet.

<input required />

Types of values

Browsers will validate certain input types to make sure the value looks correct. For example:

<!-- checks the value is an email string -->
<input type="email" required />
<!-- checks the value is a URL string -->
<input type="url" required />

Matching a pattern

We can specify a regex the value must match using the pattern attribute. For example this input will be invalid if it contains whitespace characters:

<input type="text" pattern="\S" />

Other validation

Here’s a full list of validation attributes.

You can even style inputs based on their validity using CSS pseudo-classes like :invalid, :valid and :required.

Challenge

Ensure each input meets our validation requirements above. If you submit the form with invalid values the browser will automatically stop the submission and show a warning.

Hint: Here’s a regexp for validating that a string contains at least one number: .*\d.*

Toggle answer
<input id="email" type="email" required />
<!-- ... labels etc -->
<input
id="password"
type="password"
aria-describedby="passwordRequirements"
required
pattern=".*\d.*"
minlength="8"
/>

Custom validation

Built-in validation is very simple to implement, and it works without JavaScript. However it has a few downsides. We cannot style the error message bubbles that pop up. The messages are not properly exposed to most screen readers. Required inputs are marked invalid as soon as the page loads (since they are empty). We can definitely improve this user experience by enhancing our validation with JS.

It’s still useful to start with the HTML5 validation attributes, so that if our JS fails to load or breaks the user at least gets basic validation.

Hijacking the built-in validation

First we need to disable the native validation by setting the novalidate attribute on the form element. This prevents the built-in errors from appearing.

Then we can listen for the form’s submit event and check whether any inputs are invalid using the form element’s .checkValidity() method.

This method returns true if all inputs are valid, otherwise it returns false. If any of the inputs are invalid we want to call event.preventDefault() to stop the form from submitting. Don’t worry about showing error messages for now.

Challenge

  1. Open workshop/index.js
  2. Disable the native form validation
  3. Listen for submit events and check whether all the inputs are valid
  4. Prevent the form from submitting if any inputs are invalid
Toggle answer
const form = document.querySelector("form");
form.setAttribute("novalidate", "");

form.addEventListener("submit", (event) => {
const allInputsValid = form.checkValidity();
if (!allInputsValid) {
event.preventDefault();
}
});

Handling invalid inputs

We’ve managed to stop the form submitting invalid values, but we need to provide feedback to the user so they can fix their mistakes.

First we need to actually mark the input as “invalid”. The aria-invalid attribute does this. Each input should have aria-invalid="false" set at first, since the user hasn’t typed anything yet. Then we need to know when the input becomes invalid, so we can update to aria-invalid="true".

We can listen for an input’s invalid event to run code when it fails validation. The browser will fire this event for all invalid inputs when you call the form element’s checkValidity() method. E.g.

inputElement.addEventListener("invalid", (event) => {
console.log(inputElement + " is now invalid");
});

The final step is showing a validation message depending on what type of validation error occurred. We can access the default browser message via the input.validationMessage property. E.g. for a required input this might be "Please fill out this field".

Challenge

  1. Loop through all the inputs
  2. Mark each as valid
  3. For each input listen for the invalid event
    • Mark the input as invalid when this event fires

Hint: you can use the element.setAttribute method to set ARIA attributes.

Toggle answer
const inputs = form.querySelectorAll("input");

inputs.forEach((input) => {
input.setAttribute("aria-invalid", false);
input.addEventListener("invalid", () => {
input.setAttribute("aria-invalid", true);
console.log(input.validationMessage);
});
});

Validation messages

We need to actually tell the user what their mistake was. The simplest way to do this is to grab the built-in validation message from the browser. This will be available as the element.validationMessage property. For example if the user typed “hello” into this input:

<input type="email" />
<script>
input.addEventListener("invalid", () => {
console.log(input.validationMessage);
});
</script>

The JS would log something like “Please include an ‘@’ in the email address”. These message vary across browsers.

We need to put the message on the page so the user knows what they did wrong. The message should be associated with the correct input: we want it to be read out by a screen reader when the user focuses the input.

We can achieve this using aria-describedby just like with our password requirements. This can take multiple IDs for multiple descriptions (the order of the IDs determines the order they will be read out).

<label for="password">Password</label>
<div id="passwordRequirements">
Please enter at least 8 characters containing at least 1 number
</div>
<input
id="password"
type="password"
aria-describedby="passwordRequirements passwordError"
required
/>

<div id="passwordError">{insert the right message in here}</div>

Whenever this input is focused a screen reader will read out the label first, then the type of input, then any ARIA descriptions.

Challenge

  1. Create divs to contain the error messages
  2. Set attributes on the inputs and divs so they are linked together
  3. Put the validation messages inside the divs so the user can read them
Toggle answer
<!-- lots of stuff removed to simplify example -->

<input id="email" aria-describedby="emailError" />
<div id="emailError"></div>

<input id="password" aria-describedby="passwordRequirements passwordError" />
<div id="passwordError"></div>
inputs.forEach((input) => {
// ...
input.addEventListener("invalid", () => {
// ...
const errorId = input.id + "Error";
// find the div we're putting the error in
const errorContainer = form.querySelector("#" + errorId);
errorContainer.textContent = input.validationMessage;
});
});

Re-validating

Right now it’s a little confusing for the user as the input stays marked invalid even when they type something new. We should mark each input as valid and remove the error message when the user inputs something.

  1. Add an event listener for input events
  2. Mark the input valid and remove the error message
Toggle answer
inputs.forEach((input) => {
// ...
input.addEventListener("input", () => {
input.setAttribute("aria-invalid", false);

const errorId = input.id + "Error";
// find the div we're removing the error from
const errorContainer = form.querySelector("#" + errorId);
errorContainer.textContent = "";
});
});

Styling

We have a functional, accessible solution now, but it could be improved with some styling. It’s common to style validation messages with a “danger” colour like red, and sometimes to mark invalid inputs with a different coloured border. You could also use warning icons to make errors even more obvious.

Challenge

  1. Style the error messages
  2. Style invalid inputs
  3. Add any other styles you like to make it look good

Hint: you can target elements in CSS by their attributes:

div[some-attribute="true"] {
color: red;
}
Toggle answer
input[aria-invalid="true"] {
border-color: hsl(340, 70%, 50%);
}

.error {
margin-top: 0.5rem;
color: hsl(340, 70%, 50%);
}

initial solution

Stretch goal: custom messages

The default browser messages could be better. They don’t contain specific, actionable feedback. E.g. if a pattern doesn’t match the user sees “Please match the requested format”. It would be more useful to show “Please enter at least one number”.

We need to know what type of error occurred to show the right custom message. The input element’s .validity property contains this information.

This interface has properties for every kind of error. For example an empty required input will show:

{
valueMissing: true,
typeMismatch: false,
patternMismatch: false,
// ... etc
}

Here’s a list of all the validity properties.

We can write an if/else statement to check whether each property we’re interested in is true. If it is we can show a custom error on the page:

let message = "";
if (validity.valueMissing) {
message = "Please enter an email address";
} else if (validity.typeMismatch) {
// ...
}

Challenge

  1. Edit your invalid handler to check the validity interface
  2. Show custom error messages based on the input’s ID and what validation failed.

Final solution

Resources