Most JavaScript form validation libraries are large, and often require other libraries like jQuery. For example, MailChimp's embeddable form includes a 140kb validation file (minified). It includes the entire jQuery library, a third-party form validation plugin, and some custom MailChimp code. In fact, that setup is what inspired this new series about modern form validation. What new tools do we have these days for form validation? What is possible? What is still needed?
In this series, I'm going to show you two lightweight ways to validate forms on the front end. Both take advantage of newer web APIs. I'm also going to teach you how to push browser support for these APIs back to IE9 (which provides you with coverage for 99.6% of all web traffic worldwide).
Finally, we'll take a look at MailChimp's sign-up form, and provide the same experience with 28× less code.
It's worth mentioning that front-end form validation can be bypassed. You should always validate your code on the server, too.
Alright, let's get started!
Article Series:
- Constraint Validation in HTML (You are here!)
- The Constraint Validation API in JavaScript
- A Validity State API Polyfill
- Validating the MailChimp Subscribe Form
The Incredibly Easy Way: Constraint Validation
Through a combination of semantic input types (for example, <input type="email">
) and validation attributes (such as required
and pattern
), browsers can natively validate form inputs and alert users when they're doing it wrong.
Support for the various input types and attributes varies wildly from browser to browser, but I'll provide some tricks and workarounds to maximize browser compatibility.
Basic Text Validation
Let's say you have a text field that is required for a user to fill out before the form can be submitted. Add the required
attribute, and supporting browsers will both alert users who don't fill it out and refuse to let them submit the form.
<input type="text" required>
Do you need the response to be a minimum or maximum number of characters? Use minlength
and maxlength
to enforce those rules. This example requires a value to be between 3 and 12 characters in length.
<input type="text" minlength="3" maxlength="12">
The pattern
attribute let's you run regex validations against input values. If you, for example, required passwords to contain at least 1 uppercase character, 1 lowercase character, and 1 number, the browser can validate that for you.
<input type="password" pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$" required>
If you provide a title
attribute with the pattern
, the title
value will be included with any error message if the pattern doesn't match.
<input type="password" pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$" title="Please include at least 1 uppercase character, 1 lowercase character, and 1 number." required>
You can even combine it with minlength
and (as seems to be the case with banks, maxlength
) to enforce a minimum or maximum length.
<input type="password" minlength="8" pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$" title="Please include at least 1 uppercase character, 1 lowercase character, and 1 number." required>
See the Pen Form Validation: Basic Text by Chris Ferdinandi (@cferdinandi) on CodePen.
Validating Numbers
The number
input type only accepts numbers. Browsers will either refuse to accept letters and other characters, or alert users if they use them. Browser support for input[type="number"]
varies, but you can supply a pattern
as a fallback.
<input type="number" pattern="[-+]?[0-9]">
By default, the number
input type allows only whole numbers.
You can allow floats (numbers with decimals) with the step
attribute. This tells the browser what numeric interval to accept. It can be any numeric value (example, 0.1
), or any
if you want to allow any number.
You should also modify your pattern
to allow decimals.
<input type="number" step="any" pattern="[-+]?[0-9]*[.,]?[0-9]+">
If the numbers should be between a set of values, the browser can validate those with the min
and max
attributes. You should also modify your pattern
to match. For example, if a number has to be between 3 and 42, you would do this:
<input type="number" min="3" max="42" pattern="[3-9]|[1-3][0-9]|4[0-2]">
See the Pen Form Validation: Numbers by Chris Ferdinandi (@cferdinandi) on CodePen.
Validating Email Addresses and URLs
The email
input type will alert users if the supplied email address is invalid. Like with the number
input type, you should supply a pattern for browsers that don't support this input type.
Email validation regex patterns are a hotly debated issue. I tested a ton of them specifically looking for ones that met RFC822 specs. The one used below, by Richard Willis, was the best one I found.
<input type="email" pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$">
One "gotcha" with the the email
input type is that it allows email addresses without a TLD (the "example.com" part of "email@example.com"). This is because RFC822, the standard for email addresses, allows for localhost emails which don't need one.
If you want to require a TLD (and you likely do), you can modify the pattern
to force a domain extension like so:
<input type="email" title="The domain portion of the email address is invalid (the portion after the @)." pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$">
Similarly, the url
input type will alert users if the supplied value is not a valid URL. Once again, you should supply a pattern for browsers that don't support this input type. The one included below was adapted from a project by Diego Perini, and is the most robust I've encountered.
<input type="url" pattern="^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,})?(?:[\/?#]\S*)?$">
Like the email
attribute, url
does not require a TLD. If you don't want to allow for localhost URLs, you can update the pattern to check for a TLD, like this.
<input type="url" title="The URL is a missing a TLD (for example, .com)." pattern="^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*(?:\.(?:[a-zA-Z\u00a1-\uffff]{2,}))\.?)(?::\d{2,})?(?:[/?#]\S*)?$">
See the Pen Form Validation: Email & URLs by Chris Ferdinandi (@cferdinandi) on CodePen.
Validating Dates
There are a few really awesome input types that not only validate dates but also provide native date pickers. Unfortunately, Chrome, Edge, and Mobile Safari are the only browsers that implement it. (I've been waiting years for Firefox to adopt this feature! Update: this feature should hopefully be coming to Firefox in the near future, too.) Other browsers just display it as a text field.
As always, we can provide a pattern
to catch browsers that don't support it.
The date
input type is for standard day/month/year dates.
<input type="date" pattern="(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))">
In supporting browsers, the selected date is displayed like this: MM/DD/YYYY
(caveat: in the US. This can vary for users in other countries or who have modified their date settings). But the value
is actually in this format: YYYY-MM-DD
.
You should provide guidance to users of unsupported browsers about this format—something like, "Please use the YYYY-MM-DD format." However, you don't want people visiting with Chrome or Mobile Safari to see this since that's not the format they'll see, which is confusing.
See the Pen Form Validation: Dates by Chris Ferdinandi (@cferdinandi) on CodePen.
A Simple Feature Test
We can write a simple feature test to check for support, though. We'll create an input[type="date"]
element, add a value that's not a valid date, and then see if the browser sanitizes it or not. You can then hide the descriptive text for browsers that support the date
input type.
<label for="date">Date <span class="description-date">YYYY-MM-DDD</span></label>
<input type="date" id="date" pattern="(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))">
<script>
var isDateSupported = function () {
var input = document.createElement('input');
var value = 'a';
input.setAttribute('type', 'date');
input.setAttribute('value', value);
return (input.value !== value);
};
if (isDateSupported()) {
document.documentElement.className += ' supports-date';
}
</scipt>
<style>
.supports-date .description-date {
display: none;
}
</style>
See the Pen Form Validation: Dates with a Feature Test by Chris Ferdinandi (@cferdinandi) on CodePen.
Other Date Types
The time
input type let's visitors select a time, while the month
input type let's them choose from a month/year picker. Once again, we'll include a pattern for non-supporting browsers.
<input type="time" pattern="(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9])">
<input type="month" pattern="(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2]))">
The time
input displays time in 12-hour am/pm format, but the value
is 24-hour military time. The month
input is displayed as May 2017
in supporting browsers, but the value is in YYYY-MM
format.
Just like with input[type="date"]
, you should provide a pattern description that's hidden in supporting browsers.
See the Pen Form Validation: Add `novalidate` programatically by Chris Ferdinandi (@cferdinandi) on CodePen.
This seems super easy. What's the catch?
While the Constraint Validation API is easy and light-weight, it does have some drawbacks.
You can style fields that have errors on them with the :invalid
pseudo-selector, but you can't style the error messages themselves.
Behavior is also inconsistent across browsers. Chrome doesn't display any errors until you try to submit the form. Firefox displays a red border when the field loses focus, but only displays error messages on hover (whereas WebKit browsers keep the errors persistent).
User studies from Christian Holst and Luke Wroblewski (separately) found that displaying an error when the user leaves a field, and keeping that error persistent until the issue is fixed, provided the best and fastest user experience. Bonus CSS tip: style invalid selectors only when they aren't currently being edited with :not(:focus):invalid { }
.
Unfortunately, none of the browsers behave exactly this way by default.
In the next article in this series, I'll show you how to use the native Constraint Validation API to bolt-in our desired UX with some lightweight JavaScript. No third-party library required!
Article Series:
- Constraint Validation in HTML (You are here!)
- The Constraint Validation API in JavaScript
- A Validity State API Polyfill
- Validating the MailChimp Subscribe Form
Great introduction !
You might be planning to cover this in your next post, but you can force Chrome to validate fields on lost focus (or force any browser to validate fields at any time) using the
checkValidity()
function: https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/checkValidityEdit: It looks like support for this is a little more complex than I’d thought. HTMLInputElement has it too, but HTMLFormElement has
reportValidity()
instead, for some reason. Not sure about other form field elements.In the next article I talk about Validity State, which does the same thing as
checkValidity()
but with more/better features.We’ll build a JavaScript-enhanced validator around our Native Constraint Validation attributes without having to write a massive library with tons of regex patterns.
And, addressing your concern about support, in the third article we’ll create a lightweight polyfill to bolt in missing features to all browsers and push support all the way back to IE9.
Great to see a comprehensive rundown of this often overlooked corner of front-end development. I almost always have to implement a form on a website, and my validation methods are always changing. It’s nice to see a central place with a lot of overview from well-tested methods.
I love this article! As someone who is working extensively with form validations and input masking while trying to keep the JS load under control, it’s great to see a thorough accounting of native HTML5 capabilities. I do have one observation about this passage
I’d like to point out that MS Edge has a great
input type="date"
native date picker as well.My Mac bias is showing here. Just updated the article. Thanks for pointing this out!
The wait for Firefox is about to be over, at least: Date/Time Inputs Enabled on Nightly
Thanks! I’d heard about this but couldn’t find a link to confirm at time of writing. Just updated the article accordingly.
Great article, however, don’t you think that non-native form validation is used for more flexible solutions? For instance, it is impossible to have other than English error messages.
Totally agree! I think the constraint validation attributes alone are insufficient, and the issue you bring up—non-English errors—is an important one that I didn’t mention (and should have).
Tomorrow in part 2, I’m going to walk through how we can use a browser-native JavaScript API to create a super lightweight custom validation script that gives you the flexibility to customize errors, change how and when they display, and more.
I’d still consider it “native,” but it’s more robust than the pure HTML version, and way smaller than what you often get using a completely non-native plugin.
One thing I’ve always found interesting, is that minlength can be bypassed if the user uses multi-byte characters (such as emojis). minlength and maxlength actually refer to the number of bytes, and not the number of characters, so “☹️” has a length of 2.
This could also cause problems for languages that regularly use multi-byte characters.
Nice article. A small addition to Bonus CSS tip.
To style invalid selectors only when they aren’t focused and are empty (but have a placeholder) you can do the following:
Note: IE and Edge don’t support
:placeholder-shown
“[…] 28× (2,800%) less code.”
That’s not how math works. If the other code is 28x as much as yours, your code is 1/28th, or 3.6% of the other code. That is 96.4% less.
2800% less would mean: if the other code base has 100 lines, yours has -2700.
</smart ass mode off>
Thanks Dominic! I literally struggled with how to calculate the percentage there, and clearly failed. I just removed the percentage.
Nice write-up Chris.
Are you going to add ARIA support via JavaScript in your next article?
In part 2, I touch on how to associate error messages with their respective fields using
aria-describedby
. That’s about as deep as I go, though.The pattern for numbers between 3 and 42 is wrong:
[3-42]
will only match “2”, “3” or “4”.You’d need to use something like
([3-9]|[1-3][0-9]|4[0-2])
instead.Thank you! Clearly the dark art of regex is not my strong suite. I really appreciate this. Just updated the post.
Nice. Look like the patterns for TLD only accept 2..3 characters, whereas they could be longer: According to RFC 1034 TLD length can be up to 63 octets.
Good catch, Paul! I noticed that while writing the series, and thought I had fixed it everywhere, but missed a spot or two. Fixed!
With date fields, the displayed format is taken from the user’s operating system settings. There’s no guarantee that they will be in the US format of MM/DD/YYYY.
For example, I have dates formatted as YYYY-MM-DD on my computer, and that’s how they display in the field in Chrome.
Thanks for the clarification. I just updated the post accordingly.
Then how to do i18n localization?
It looks like they localize automatically via the user’s browser locale, which may or may not be the most accurate way to do it.
In Part 2, I provide a JavaScript solution that let’s you customize both the location and text of the messages, which may be a better option for you.