Filling out forms on the web isn't the most exciting activity in the world, especially when you enter something the website doesn't expect. Most websites do a pretty poor job giving you feedback. For example, you submit your data, it reloads the page, deletes half the things you entered, and tells you "Please enter a password with 2 lower case letters, 4 uppercase letters, a number, and an even number of %^&# characters". 🤦 Lol & thanks, every travel or banking website I have ever used.

This feedback is called form validation. An example:

good luck submitting this Password Submit

Many websites still roll their own form validation system, and making a something good from scratch is not a simple affair.

Obviously, your users deserve a good experience when they get something wrong. And you deserve minimal pain in building out this experience.

Fortunately today's browsers have form validation features built in. These features are pretty good now and are actually supported by 🎉 all current browsers 🎉. As with many browser features, there are warts. But you can work around them with a small amount of code and have something that feels good.

I'm hoping this post exposes a couple more people to HTML5 form validation, or shows folks the current capabilities if they haven't looked at these features in a while.

Just want the TL;DR and the code? Check out the valid-form project; a JavaScript library using the techniques from this blog post. You can also see it in action.

Example 1: The Basics

There are three categories of HTML5 form validation features:

HTML attributes on <input> , <select> , and <textarea> elements. (From here on out, I will just refer to them all as <input> cause brevity)

, , and elements. (From here on out, I will just refer to them all as cause brevity) CSS pseudo selectors

JavaScript APIs

And you can choose your own adventure. The only thing required to start validating is using a couple HTML attributes, which is crazy easy. So let's start there.

We'll add a required attribute to an <input> to ensure the user needs to put something in the email address box. We'll also set the input's type="email" which will render a text box but only allow email addresses. Notice you cannot submit the form unless you fill it out with an email address. This is just HTML, no JavaScript!

basic example Submit this empty or with an invalid email. Your Email Submit

<form> <label>Your Email</label> <!-- `required` and `type="email"` does everything! --> <input required type="email" name="email" placeholder="a@b.com" /> </form>

Boom💥! 20 characters in HTML gave us validation! Now that we have it working, you might have a couple questions:

How do I style the <input> when it is valid or invalid? Can I specify custom error messages? If so, how? How do I style the error message?

So let's dig into each one of these with an example. We will build step-by-step on the simple example above until we have something more custom and attractive.

Example 2: Styling the input

There are a couple CSS pseudo classes supported by each <input> . Two potentially useful selectors in this situation:

:valid - when the input is valid

- when the input is valid :invalid - when the input is invalid

The most basic approach would be to change the border color when the input is invalid on our previous example:

/* CSS :invalid pseudo selector */ input:invalid { border-color: red; }

basic example + :invalid styling The box is red the whole time you are typing an email. Weak. :/ Your Email Submit

Notice, though, that it isn't very good. What is even happening?

The input is required and initially empty, so on page load the input is :invalid and border is red. :( :( The input is :invalid until you type a legit email address, so it is red when typing. :( :(

Ok, so the :invalid pseudo selector is kind of useless. Ideally we would not show an error up front or the whole time they are typing, just when they submit something unacceptable.

Dave Rupert has a good solution that uses an input's invalid event. Instead of using the :invalid pseudo selector, he adds a CSS class to the input when it becomes invalid. We will extend this approach by removing the CSS class when the element becomes valid again.

The CSS

/* .invalid CSS class instead or pseudo selector */ input.invalid { border-color: red; }

And the JavaScript will add the invalid class when the input is invalid

var invalidClassName = 'invalid' var inputs = document.querySelectorAll('input, select, textarea') inputs.forEach(function (input) { // Add a css class on submit when the input is invalid. input.addEventListener('invalid', function () { input.classList.add(invalidClassName) }) // Remove the class when the input becomes valid. // 'input' will fire each time the user types input.addEventListener('input', function () { if (input.validity.valid) { input.classList.remove(invalidClassName) } }) })

basic example + .invalid CSS class Submit this empty or with an invalid email. Your Email Submit

Better, eh?

This behavior feels better because you are punished with an error only after you try to submit with a unacceptable answer, not while you are typing a potentially legit answer. Plus you get immediate feedback when you correct an error. 🎉

Example 3: Custom Messages

The browser provides default error messages. But they are super generic—"Please fill out this field", etc.—and may not be what you want.

Generic as they may be, there is an upside to sticking with the default error messages: default errors are translated into the user's local language. e.g. If a user is reading your site in German, the default error messages will be in German also.

The key to custom messages is using an input HTMLElement's setCustomValidity function, e.g. input.setCustomValidity('My custom message') . You can call this function at any time. If you pass a non-empty string, it will treat the input as invalid and not allow form submission. It is a flexible function. For example, it can be used to build your own validation for custom types.

But setCustomValidity is a bit cumbersome for custom messages as you can't just override a specific message. It's "show this error no matter what" or nothing.

<input> HTMLElements also have a validity object that tells you what, if anything, on the element is invalid. We can use input.validity in conjunction with input.setCustomValidity() to override specific messages.

// The keys (e.g. valueMissing) map onto // a key in the `input.validity` object const customMessages = { valueMissing: 'Custom required!', // `required` attr emailMismatch: 'Custom email mismatch', // Invalid email patternMismatch: 'Custom pattern mismatch',// `pattern` attr } function getCustomMessage (type, validity) { if (validity.typeMismatch) { return customMessages[`${type}Mismatch`] } else { for (const invalidKey in customMessages) { if (validity[invalidKey]) { return customMessages[invalidKey] } } } } var inputs = document.querySelectorAll('input, select, textarea') inputs.forEach(function (input) { // Each time the user types or submits, this will // check validity, and set a custom message if invalid. function checkValidity () { const message = input.validity.valid ? null : getCustomMessage(input.type, input.validity, customMessages) input.setCustomValidity(message || '') } input.addEventListener('input', checkValidity) input.addEventListener('invalid', checkValidity) })

example with custom messages This email field accepts a and b chars before the @ symbol. Your Email Submit

You could extend this technique even further by passing in a special customMessages object for each <input> , or having it read custom attributes on the <input> containing the messages. FWIW, valid-form (a JS lib based on this post) does support custom attributes.

Example 4: Custom Message Rendering

Unfortunately the browser doesn't give you any control over styling the default error message display, and they are (of course) different on every browser.

But you can render your own element when there is an error. Basically, you just hook the same events as the other examples, and insert a div wherever you like with the error. This example is the same as above, but it will show the error message next to the label instead of as a tooltip thing under the input.

example with custom messages + rendering This email field accepts a and b chars before the @ symbol. Your Email Submit

const validationErrorClass = 'validation-error' const parentErrorClass = 'has-validation-error' const inputs = document.querySelectorAll('input, select, textarea') inputs.forEach(function (input) { function checkValidity (options) { const insertError = options.insertError const parent = input.parentNode const error = parent.querySelector(`.${validationErrorClass}`) || document.createElement('div') if (!input.validity.valid && input.validationMessage) { error.className = validationErrorClass error.textContent = input.validationMessage if (insertError) { parent.insertBefore(error, input) parent.classList.add(parentErrorClass) } } else { parent.classList.remove(parentErrorClass) error.remove() } } input.addEventListener('input', function () { // We can only update the error or hide it on input. // Otherwise it will show when typing. checkValidity({insertError: false}) }) input.addEventListener('invalid', function (e) { // prevent showing the default display e.preventDefault() // We can also create the error in invalid. checkValidity({insertError: true}) }) })

Get The Code

I created a small JavaScript library that rolls up all three techniques from this post so you dont have to: valid-form. You can also see it in action.

Server Validation

The HTML5 form validation techniques in this post only work on the front end. Someone could turn off JavaScript and still submit jank data to a form with the tightest JS form validation.

To be clear, you should still do validation on the server. How you display those errors to a user is up to you. With these JavaScript techniques, the display of server validation errors could be a lot simpler if you expect most of your users to have JS enabled. For example, Rails still encourages you to dump all validation errors at the top of a form, which is lulzy in this age of touchy UX. But you could do that minimal thing with server errors, then rely on HTML5 validation to provide a good user experience for the vast majority of your users.

Reference

The examples in this post didn't show absolutely all the validation possibilities. Here are a couple quick tables that hopefully help in your validation quest.

HTML Validation Attributes

Using these attributes will show validation errors, or limit what the user can enter into an <input> .

Attribute Notes <input required> Ensure the user enters at least one character <input minlength="2"> Ensure the <input> has a length greater than or equal to the value specified <input maxlength="10"> Ensure the <input> has a length less than or equal to the value specified <input pattern="[a-z]+"> Match a regular expression <input min="2"> Number value must be >= the value. Only works on type="number" or type="range" inputs! <input max="10"> Number value must be <= the value. Only works on type="number" or type="range" inputs! <input step="2"> Number value must be a multiple of this number. Only works on type="number" or type="range" inputs! <form novalidate> Disable HTML5 form validation for the form.

HTML Type Attributes

In addition to the validation attributes, you should use the correct type attribute for your input. e.g. for email addresses, you should use type="email" , for numbers, you should use type="number" . Some types provide extra validation, and will show a message when the input does not match the type. These types will show error messages:

Type Notes email Input must be a valid email address. url Input must be a URL that starts with http . number Input must be a number.

There are many more acceptable values for the type field. Using the correct type may save you from having to validate an input at all, yay.

CSS Pseudo Classes

You can style <input> s in various states with a couple pseudo selectors.

Pseudo Selector Notes :valid When the input is valid :invalid When the input is invalid :required When the input has a required attribute :optional When the input does not have a required attribute

Validity State

Remember the object with custom messages above?

const customMessages = { valueMissing: 'Custom required!', // `required` attr patternMismatch: 'Custom pattern mismatch',// `pattern` attr }

valueMissing etc. are based on an <input> 's input.validity object. e.g. input.validity.valueMissing is true when an <input> requires data from the user.

Here is a table of all values reproduced (with clarifying changes) from the MDN ValidityState docs: