In this workshop you'll learn how to validate user input in the browser and present error messages accessibly.
- Clone this repo
- Open
workshop/index.html
in your browser - This is the form we'll be adding validation to
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.
Our form has two inputs: one for an email address and one for a password. These are the requirements we need to validate:
- Both values are present
- The email value is a valid email address
- 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.
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.
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.
- Add a visual required indicator to both inputs.
- Add instructions containing our password requirements
- 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".
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
.
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"
/>
- Very simple to implement
- Works without JavaScript
- 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 arequired
input red straight away
- E.g.
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.
- Open
workshop/index.js
- Disable the native form validation
- 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();
}
});
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"
.
- Get a reference to all the inputs
- Mark each input as valid
- For each input listen for the
invalid
event - 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);
}
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".
- Create a div to contain the message
- Set attributes on the input and div so they are linked together
- 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;
}
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.
- Add an event listener for
input
events - 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 = "";
}
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.
- Style the error messages
- Style invalid inputs
- 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%);
}
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) {
// ...
}
- Edit your
invalid
handler to check thevalidity
interface - Show custom error messages based on the input's ID and what validation failed.
- Constraint Validation | MDN (built-in HTML validation)
- Native form validation—part 1 (the limitations of HTML/CSS-only validation)
- Required attribute requirements (explains the JS/ARIA stuff we need for accessible validation)
- Describing aria-describedby (summarises how
aria-describedby
works)