Post hero image

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

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)
  • 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.

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

  1. How do I style the <input> when it is valid or invalid?
  2. Can I specify custom error messages? If so, how?
  3. 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
  • :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. :/

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

  1. The input is required and initially empty, so on page load the input is :invalid and border is red. :( :(
  2. 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.

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.

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.

different error uis; lol thanks, vendors

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.

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

AttributeNotes
<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:

TypeNotes
emailInput must be a valid email address.
urlInput must be a URL that starts with http.
numberInput 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 SelectorNotes
:validWhen the input is valid
:invalidWhen the input is invalid
:requiredWhen the input has a required attribute
:optionalWhen 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:

PropertyNote
validBoolean indicating the element meets all constraint validations, and is therefore considered to be valid. When true, all of the following properties in this table will be false. When input.validity.valid === false, one or more of the properties will be true.
badInputBoolean indicating the user has provided input that the browser is unable to convert.
patternMismatchBoolean indicating the value does not match the regex pattern specified in the pattern attribute.
rangeOverflowBoolean indicating the value is greater than the maximum specified by the max attribute.
rangeUnderflowBoolean indicating the value is less than the minimum specified by the min attribute.
stepMismatchBoolean indicating the value does not fit the rules determined by the step attribute (that is, it's not evenly divisible by the step value).
tooLongBoolean indicating the value exceeds the specified maxlength attribute for HTMLInputElement or HTMLTextAreaElement objects. Note: This will never be true in Gecko, because elements' values are prevented from being longer than max attribute.
tooShortBoolean indicating the value fails to meet the specified minlength attribute for HTMLInputElement or HTMLTextAreaElement objects.
typeMismatchBoolean indicating the value is not in the required syntax (when type is email or url).
valueMissingBoolean indicating the element has a required attribute, but no value.

← back to all posts