/learn-form-validation

Learn how to validate user input accessibly

Primary LanguageJavaScript

Learn form validation

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

Final solution

Setup

  1. Clone this repo
  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 devtools 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 actually implement validation we need to make sure the user is aware of the requirements. 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 isn't read out.

Different types of value

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

We can add a <div> with this information after the label, but this won't be associated with the input (which means screen readers will ignore it).

We need to use the aria-describedby attribute on the input. This attribute takes the IDs of other elements that provide additional info. This will link our 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
Solution
<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 pretty complex validation on inputs:

<input type="email" required />

When a form containing the above input is submitted the browser will validate both that the user entered a value (because of the required) and that the value is an email (because of the type="email").

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

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

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 should stop the submission and show a warning.

Hint if you don't like regexp

Here's a regular expression for validating that a string contains at least one number: .*\d.*

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

Advantages

  • Very simple to implement
  • Works without JavaScript

Disadvantages

  • Cannot style the error messages
  • Not exposed to most screen readers
  • Inputs are marked invalid before the user types anything
    • E.g. input:invalid { border-color: red; } will mark a required input red straight away

Custom validation

The advantage of starting with the HTML5 validation attributes is that if our JS fails to load or breaks the user at least gets basic 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 formElement.checkValidity().

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.

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
    • If they are not prevent the form from submitting
Solution
const form = document.querySelector("form");
form.setAttribute("novalidate", "");

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

Handling invalid inputs

We need to communicate whether inputs are valid or invalid to assistive tech. The aria-invalid attribute does this. Each input should have aria-invalid="false" set at first, since the user hasn't typed anything yet. We'll update it to true when the input fails validation.

The checkValidity() method causes inputs that failed validation to fire an invalid event. We can add event listeners for this to our inputs, allowing us to run custom JS to show the right error message.

inputElement.addEventListener("invalid", handleInvalidInput);

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. Get a reference to all the inputs
  2. Mark each input as valid
  3. For each input listen for the invalid event
  4. Mark the input as invalid and log its validation message
Hint

You can access the element that triggered an event using event.target inside the event handler function.

Solution
const inputs = form.querySelectorAll("input");

inputs.forEach((input) => {
  input.setAttribute("aria-invalid", false);
  input.addEventListener("invalid", handleInvalidInput);
});

function handleInvalidInput(event) {
  const input = event.target;
  input.setAttribute("aria-invalid", true);
  console.log(input.validationMessage);
}

Showing the validation message

We need to put the validation 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. It can take multiple IDs for multiple descriptions (the order of the IDs determines the order they will be read out).

<input id="email" type="email" aria-describedby="emailError" required />
<div id="emailError">Please enter an email address</div>

Whenever this input is focused a screen reader will read out the label first, then the type of input, then any description. So in this case something like "Email, required invalid data, edit text. (pause) Please enter an email address".

Challenge

  1. Create a div to contain the message
  2. Set attributes on the input and div so they are linked together
  3. Put the validation message inside the div so the user can read it
Solution
<!-- 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>
function handleInvalidInput(event) {
  // ...
  const errorId = input.id + "Error";
  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
Solution
inputs.forEach((input) => {
  // ...
  input.addEventListener("input", clearValidity);
});

function clearValidity(event) {
  const input = event.target;

  input.setAttribute("aria-invalid", false);

  const errorId = input.id + "Error";
  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;
}
Solution
/* lots of styles omitted for clarity */

input[aria-invalid="true"] {
  border-color: hsl(340, 70%, 50%);
}

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

initial solution

Stretch: 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.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