Constraint validation
Native client side validation for web forms
Introduction #
Validating forms has notoriously been a painful development experience. Implementing client side validation in a user-friendly, developer-friendly, and accessible way is hard. Before HTML5 there was no means of implementing validation natively; therefore, developers have resorted to a variety of JavaScript based solutions.
To help ease the burden on developers, HTML5 introduced a concept known as constraint validation - a native means of implementing client side validation on web forms.
Yet, despite being available in the latest version of all major browsers, constraint validation is a topic largely relegated to presentations and demos. My goal in writing this is to shed some light on the new APIs to help developers make better web forms for everyone.
In this tutorial I will:
- Present a comprehensive overview of what constraint validation is.
- Dig into the current limitations of the spec and browser implementations.
- Discuss how you can use HTML5 constraint validation in your forms now.
What is Constraint Validation? #
The core of constraint validation is an algorithm browsers run when a form is submitted to determine its validity. To make this determination, the algorithm utilizes new HTML5 attributes min
, max
, step
, pattern
, and required
as well as existing attributes maxlength
and type.
As an example take this form with an empty required
text input:
<form>
<input type="text" required value="" />
<input type="submit" value="Submit" />
</form>
If you attempt to submit this form as is, supporting browsers will prevent the submission and display the following:







Per the spec how errors are presented to the user is left up to the browser itself. However, the spec does provide a full DOM API, new HTML attributes, and CSS hooks authors can use to customize the experience.
DOM API #
The constraint validation API adds the following properties / methods to DOM nodes.
willValidate #
- Chrome 77, Supported 77
- Firefox 98, Supported 98
- Edge 79, Supported 79
- Safari 16.4, Supported 16.4
The willValidate
property indicates whether the node is a candidate for constraint validation. For submittable elements this will be set to true
unless for some reason the node is barred from constraint validation, such as possessing the disabled
attribute.
<div id="one"></div>
<input type="text" id="two" />
<input type="text" id="three" disabled />
<script>
document.getElementById('one').willValidate; //undefined
document.getElementById('two').willValidate; //true
document.getElementById('three').willValidate; //false
</script>
validity #
- Chrome 77, Supported 77
- Firefox 98, Supported 98
- Edge 79, Supported 79
- Safari 16.4, Supported 16.4
The validity
property of a DOM node returns a ValidityState
object containing a number of boolean properties related to the validity of the data in the node.
customError
:true
if a custom validity message has been set per a call tosetCustomValidity()
.<input id="foo" />
<input id="bar" />
<script>
document.getElementById('foo').validity.customError; //false
document.getElementById('bar').setCustomValidity('Invalid');
document.getElementById('bar').validity.customError; //true
</script>
</pre>patternMismatch
:true
if the node'svalue
does not match itspattern
attribute.<input id="foo" pattern="[0-9]{4}" value="1234" />
<input id="bar" pattern="[0-9]{4}" value="ABCD" />
<script>
document.getElementById('foo').validity.patternMismatch; //false
document.getElementById('bar').validity.patternMismatch; //true
</script>rangeOverflow
:true
if the node'svalue
is greater than itsmax
attribute.<input id="foo" type="number" max="2" value="1" />
<input id="bar" type="number" max="2" value="3" />
<script>
document.getElementById('foo').validity.rangeOverflow; //false
document.getElementById('bar').validity.rangeOverflow; //true
</script>rangeUnderflow
:true
if the node'svalue
is less than itsmin
attribute.<input id="foo" type="number" min="2" value="3" />
<input id="bar" type="number" min="2" value="1" />
<script>
document.getElementById('foo').validity.rangeUnderflow; //false
document.getElementById('bar').validity.rangeUnderflow; //true
</script>stepMismatch
:true
if the node'svalue
is invalid per itsstep
attribute.<input id="foo" type="number" step="2" value="4" />
<input id="bar" type="number" step="2" value="3" />
<script>
document.getElementById('foo').validity.stepMismatch; //false
document.getElementById('bar').validity.stepMismatch; //true
</script>tooLong
:true
if the node'svalue
exceeds itsmaxlength
attribute. All browsers prevent this from realistically occurring by preventing users from inputting values that exceed the maxlength. Although rare it is possible to have this property betrue
in some browsers; I've written about how that's possible here.typeMismatch
:true
if an input node'svalue
is invalid per itstype
attribute.<input id="foo" type="url" value="http://foo.com" />
<input id="bar" type="url" value="foo" />
<input id="foo2" type="email" value="foo@foo.com" />
<input id="bar2" type="email" value="bar" />
<script>
document.getElementById('foo').validity.typeMismatch; //false
document.getElementById('bar').validity.typeMismatch; //true
document.getElementById('foo2').validity.typeMismatch; //false
document.getElementById('bar2').validity.typeMismatch; //true
</script>valueMissing
:true
if the node has arequired
attribute but has no value.<input id="foo" type="text" required value="foo" />
<input id="bar" type="text" required value="" />
<script>
document.getElementById('foo').validity.valueMissing; //false
document.getElementById('bar').validity.valueMissing; //true
</script>valid
:true
if all of the validity conditions listed above arefalse
.<input id="valid-1" type="text" required value="foo" />
<input id="valid-2" type="text" required value="" />
<script>
document.getElementById('valid-1').validity.valid; //true
document.getElementById('valid-2').validity.valid; //false
</script>
validationMessage #
- Chrome 77, Supported 77
- Firefox 98, Supported 98
- Edge 79, Supported 79
- Safari 16.4, Supported 16.4
The validationMessage
property of a DOM node contains the message the browser displays to the user when a node's validity is checked and fails.
The browser provides a default localized message for this property. If the DOM node is not a candidate for constraint validation or if the node contains valid data validationMessage
will be set to an empty string.
<input type="text" id="foo" required />
<script>
document.getElementById('foo').validationMessage;
//Chrome --> 'Please fill out this field.'
//Firefox --> 'Please fill out this field.'
//Safari --> 'value missing'
//IE10 --> 'This is a required field.'
//Opera --> ''
</script>
checkValidity() #
- Chrome 77, Supported 77
- Firefox 98, Supported 98
- Edge 79, Supported 79
- Safari 16.4, Supported 16.4
The checkValidity
method on a form element node (e.g. input, select, textarea) returns true
if the element contains valid data.
On form nodes it returns true
if all of the form's children contain valid data.
<form id="form-1">
<input id="input-1" type="text" required />
</form>
<form id="form-2">
<input id="input-2" type="text" />
</form>
<script>
document.getElementById('form-1').checkValidity(); //false
document.getElementById('input-1').checkValidity(); //false
document.getElementById('form-2').checkValidity(); //true
document.getElementById('input-2').checkValidity(); //true
</script>
Additionally, every time a form element's validity is checked via checkValidity
and fails, an invalid
event is fired for that node. Using the example code above if you wanted to run something whenever the node with id input-1
was checked and contained invalid data you could use the following:
document.getElementById('input-1').addEventListener('invalid', function() {
//...
}, false);
There is no valid event, however, you can use the change
event for notifications of when a field's validity changes.
document.getElementById('input-1').addEventListener('change', function(event) {
if (event.target.validity.valid) {
//Field contains valid data.
} else {
//Field contains invalid data.
}
}, false);
setCustomValidity() #
- Chrome 10, Supported 10
- Firefox 4, Supported 4
- Edge 12, Supported 12
- Safari 5.1, Supported 5.1
The setCustomValidity
method changes the validationMessage
property as well as allows you to add custom validation rules.
Because it is setting the validationMessage
passing in an empty string marks the field as valid and passing any other string marks the field as invalid. Unfortunately there is no way of setting the validationMessage
without also changing the validity of a field.
For example, if you had two password fields you wanted to enforce be equal you could use the following:
if (document.getElementById('password1').value != document.getElementById('password2').value) {
document.getElementById('password1').setCustomValidity('Passwords must match.');
} else {
document.getElementById('password1').setCustomValidity('');
}
HTML Attributes #
We've already seen that the maxlength
, min
, max
, step
, pattern
, and type
attributes are used by the browser to constrain data. For constraint validation there are two additional relevant attributes - novalidate
and formnovalidate
.
novalidate #
The boolean novalidate
attribute can be applied to form nodes. When present this attribute indicates that the form's data should not be validated when it is submitted.
<form novalidate>
<input type="text" required />
<input type="submit" value="Submit" />
</form>
Because the above form has the novalidate
attribute it will submit even though it contains an empty required input.
formnovalidate #
The boolean formnovalidate
attribute can be applied to button and input nodes to prevent form validation. For example:
<form>
<input type="text" required />
<input type="submit" value="Validate" />
<input type="submit" value="Do NOT Validate" formnovalidate />
</form>
When the "Validate" button is clicked form submission will be prevented because of the empty input. However, when the "Do NOT Validate" button is clicked the form will submit despite the invalid data because of the formnovalidate
attribute.
CSS Hooks #
Writing effective form validation is not just about the errors themselves; it's equally important to show the errors to the user in a usable way, and supporting browsers give you CSS hooks to do just that.
:invalid and :valid #
In supporting browsers the :valid
pseudo-classes will match form elements that meet their specified constraints and the :invalid
pseudo-classes will match those that do not.
<form>
<input type="text" id="foo" required />
<input type="text" id="bar" />
</form>
<script>
document.querySelectorAll('input[type="text"]:invalid'); //Matches input#foo
document.querySelectorAll('input[type="text"]:valid'); //Matches input#bar
</script>
Resetting Default Styling #
By default Firefox places a red box-shadow
and IE10 places a red outline
on :invalid
fields.


WebKit based browsers and Opera do nothing by default. If you would like a consistent starting point you can use the following to suppress the defaults.
:invalid {
box-shadow: none; /* FF */
outline: 0; /* IE 10 */
}
I have a pending pull request to discuss whether this normalization belongs in normalize.css.
Inline Bubbles #
A larger display discrepancy is the look of the inline validation bubbles the browser displays on invalid fields. However, WebKit is the only rendering engine that gives you any means of customizing the bubble. In WebKit you can experiment with the following 4 pseduoclasses in order to get a more custom look.
::-webkit-validation-bubble {}
::-webkit-validation-bubble-message {}
::-webkit-validation-bubble-arrow {}
::-webkit-validation-bubble-arrow-clipper {}
Removing the Default Bubble #
Because you can only customize the look of the bubbles in WebKit, if you want a custom look across all supporting browsers your only option is to suppress the default bubble and implement your own. The following will disable the default inline validation bubbles from all forms on a page.
var forms = document.getElementsByTagName('form');
for (var i = 0; i < forms.length; i++) {
forms[i].addEventListener('invalid', function(e) {
e.preventDefault();
//Possibly implement your own here.
}, true);
}
If you do suppress the default bubbles make sure that you do something to show errors to users after invalid form submissions. Currently the bubbles are the only means by which browsers indicate something went wrong.
Current Implementation Issues and Limitations #
While these new APIs bring a lot of power to client side form validation, there are some limitations to what you are able to do.
setCustomValidity #
For simply setting the validationMessage
of a field setCustomValidity
works, but as forms get more complex a number of limitations of the setCustomValidity
method become apparent.
Problem #1: Handling multiple errors on one field
Calling setCustomValidity
on a node simply overrides its validationMessage
. Therefore, if you call setCustomValidity
on the same node twice the second call will simply overwrite the first. There is no mechanism to handle for an array of error messages or a way of displaying multiple error messages to the user.
One way of handling this is to append additional messages to the node's validationMessage
as such.
var foo = document.getElementById('foo');
foo.setCustomValidity(foo.validationMessage + ' An error occurred');
You cannot pass in HTML or formatting characters so unfortunately concatenating strings can leave you with something like this:

Problem #2: Knowing when to check the validity of a field
To illustrate this issue consider the example of a form with two password input fields that must match:
<form>
<fieldset>
<legend>Change Your Password</legend>
<ul>
<li>
<label for="password1">Password 1:</label>
<input type="password" required id="password1" />
</li>
<li>
<label for="password2">Password 2:</label>
<input type="password" required id="password2" />
</li>
</ul>
<input type="submit" />
</fieldset>
</form>
My suggestion earlier was to use the change
event to implement the validation, which looks something like this:
var password1 = document.getElementById('password1');
var password2 = document.getElementById('password2');
var checkPasswordValidity = function() {
if (password1.value != password2.value) {
password1.setCustomValidity('Passwords must match.');
} else {
password1.setCustomValidity('');
}
};
password1.addEventListener('change', checkPasswordValidity, false);
password2.addEventListener('change', checkPasswordValidity, false);
Now, whenever the value of either password field is changed by the user the validity will be reevaluated. However, consider a script that automatically fills in the passwords, or even a script that changes a constraint attribute such as pattern
, required
, min
, max
, or step
. This could absolutely affect the validity of the password fields, yet, there is no event to know that this has happened.
Bottom Line: We need a means of running code whenever a field's validity might have changed.
Problem #3: Knowing when a user attempts to submit a form
Why not use the form's submit
event for the problem described above? The submit
event is not fired until after the browser has determined a form contains valid data given all of its specified constraints. Therefore, there is no way of knowing when a user attempts to submit a form and it is prevented by the browser.
It can be very useful to know when a submission attempt occurs. You may want to show the user a list of error messages, change focus, or display help text of some sort. Unfortunately, you'll need a workaround to make this happen.
One way to accomplish this is by adding the novalidate
attribute to the form and use its submit
event. Because of the novalidate
attribute the form submission will not be prevented regardless of the validity of the data. Therefore, the client script will have to explicitly check whether the form contains valid data in a submit
event and prevent submission accordingly. Here's an extension of the password match example that enforces that the validation logic will run before the form is submitted.
<form id="passwordForm" novalidate>
<fieldset>
<legend>Change Your Password</legend>
<ul>
<li>
<label for="password1">Password 1:</label>
<input type="password" required id="password1" />
</li>
<li>
<label for="password2">Password 2:</label>
<input type="password" required id="password2" />
</li>
</ul>
<input type="submit" />
</fieldset>
</form>
<script>
var password1 = document.getElementById('password1');
var password2 = document.getElementById('password2');
var checkPasswordValidity = function() {
if (password1.value != password2.value) {
password1.setCustomValidity('Passwords must match.');
} else {
password1.setCustomValidity('');
}
};
password1.addEventListener('change', checkPasswordValidity, false);
password2.addEventListener('change', checkPasswordValidity, false);
var form = document.getElementById('passwordForm');
form.addEventListener('submit', function() {
checkPasswordValidity();
if (!this.checkValidity()) {
event.preventDefault();
//Implement you own means of displaying error messages to the user here.
password1.focus();
}
}, false);
</script>
The major disadvantage of this approach is that adding the novalidate
attribute to a form prevents the browser from displaying the inline validation bubble to the user. Therefore, if you use this technique you must implement your own means of presenting error messages to the user. Here is a simple example showing one way of accomplishing this.
Bottom Line: We need a forminvalid
event that would be fired whenever a form submission was prevented due to invalid data.
Safari #
Even though Safari supports the constraint validation API, as of this writing (version 6), Safari will not prevent submission of a form with constraint validation issues. To the user Safari will behave no differently than a browser that doesn't support constraint validation at all.
The easiest way around this is to use the same approach as the workaround described above, give all forms the novalidate
attribute and manually prevent form submissions using preventDefault
. The following code adds this behavior to all forms.
var forms = document.getElementsByTagName('form');
for (var i = 0; i < forms.length; i++) {
forms[i].noValidate = true;
forms[i].addEventListener('submit', function(event) {
//Prevent submission if checkValidity on the form returns false.
if (!event.target.checkValidity()) {
event.preventDefault();
//Implement you own means of displaying error messages to the user here.
}
}, false);
}
Note: There are several documented bugs where the checkValidity
method returns false positives (see here and here for examples). False positives are especially dangerous with the above workaround because the user will be stuck on a form with valid data, so use caution.
Declarative Error Messages #
While you have the ability to change a field's error message by setting its validationMessage
through setCustomValidity
, it can be a nuisance to continuously setup the JavaScript boilerplate to make this happen, especially on large forms.
To help make this process easier, Firefox introduced a custom x-moz-errormessage
attribute that can be used to automatically set a field's validationMessage
.
<form>
<input type="text" required x-moz-errormessage="Fill this out." />
<input type="submit" value="Submit" />
</form>
When the above form is submitted in Firefox the user will see the custom message instead of the browser's default.

This feature was proposed to the W3C but was rejected. Therefore, at the moment Firefox is the only browser where you can declaratively specify error messages.
Title Attribute
While it doesn't change the validationMessage
, in the case of a patternMismatch
browsers do display the contents of the title
attribute in the inline bubble if it's provided.
For example if you try to submit the following form:
<form>
<label for="price">Price: $</label>
<input type="text" pattern="[0-9].[0-9][0-9]"
title="Please enter the price in x.xx format (e.g. 3.99)"
id="price" value="3" />
<input type="submit" value="Submit" />
</form>
Here's a sampling of what browsers will display:




:invalid and :valid #
As discussed earlier the :valid
pseudo-class will match form elements that meet all specified constraints and the :invalid
pseudo-class will match those that do not. Unfortunately these pseudo-classes will be matched immediately, before the form is submitted and before the form is interacted with. Consider the following example:
<style>
:invalid {
border: 1px solid red;
}
:valid {
border: 1px solid green;
}
</style>
<form>
<input type="text" required />
<input type="text" />
</form>
The goal here is to simply place a red border around invalid fields and a green border around valid ones, which it does immediately as the form is rendered. However, usability tests of inline form validation have shown that the best time to give user feedback is immediately after they interact with a field, not before.
One way to accomplish this with the example above is to add a class to the inputs after they have been interacted with and only apply the borders when the class is present.
<style>
.interacted:invalid {
border: 1px solid red;
}
.interacted:valid {
border: 1px solid green;
}
</style>
<form>
<input type="text" required />
<input type="text" />
<input type="submit" />
</form>
<script>
var inputs = document.querySelectorAll('input[type=text]');
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener('blur', function(event) {
event.target.classList.add('interacted');
}, false);
}
</script>
Firefox has seen these issues and implemented two additional pseudo-classes - :-moz-ui-invalid
and :-moz-ui-valid
. Unlike :invalid
and :valid
, :-moz-ui-invalid
and :-moz-ui-valid
will not match a field until the field is modified or the user tries to submit the form with invalid input.
Based on this work the CSS selectors level 4 specification contains a :user-error
pseudo-class that functions much like :-moz-ui-invalid
. It has yet to be implemented by any browsers.
One final note. Be aware that :valid
and :invalid
should match <form>
nodes in addition to form elements. Currently this has only been implemented in Firefox, but be careful when applying global rules to :valid
and :invalid
.
Unsupported Browsers #
While browser support for constraint validation is quite good, there are still some major players in which it is not present, most notably IE <= 9, iOS Safari, and the default Android browser.
Dealing with Unsupported Browsers #
If you intend to use the new constraint validation APIs in a production web form you have to do something about unsupported browsers. In my experience there are two primary options:
Option 1 - Rely on Server Side Validation Alone
It's important to remember that even with these new APIs client side validation does not remove the need for server side validation. Malicious users can easily workaround any client side constraints, and, HTTP requests don't have to originate from a browser.
Therefore client side validation should always be treated as a progressive enhancement to the user experience; all forms should be usable even if client side validation is not present.
Since you have to have server side validation anyways, if you simply have your server side code return reasonable error messages and display them to the end user you have a built in fallback for browsers that don't support any form of client side validation.
Option 2 - Polyfill
While relying on server side validation is reasonable for some applications, for many throwing away the usability advantages of client side validation for users in non-supporting browsers is simply not an option.
If you want to write code with the new APIs and want it to work anywhere the best way to accomplish this is with a polyfill. In addition to making the APIs work in non-supporting browsers, many polyfills take an extra step and workaround some of the issues in the native implementations.
Polyfilling #
There are a number of polyfills that allow use of the constraint validation API in any browser. I will discuss two of the more popular options available.
Webshims #
Webshims is collection of polyfills including one for HTML5 forms and the constraint validation API.
To show how to use Webshims let's return to our original example of a form with a single required
text input.
<form>
<input type="text" required value="" />
<input type="submit" value="Submit" />
</form>
To make this work consistently across all browsers you need to include Webshims and its dependencies then call $.webshims.polyfill('forms')
.
<!-- Webshims' dependencies -->
<script src="js/jquery-1.8.2.js"></script>
<script src="js/modernizr-yepnope-custom.js"></script>
<!-- Webshims base -->
<script src="js-webshim/minified/polyfiller.js"></script>
<script>jQuery.webshims.polyfill('forms');</script>
<form>
<input type="text" required value="" />
<input type="submit" value="Submit" />
</form>
In supporting browsers there will be no difference in the result. However, browsers that don't support constraint validation natively will now prevent invalid form submissions and display an error message in a custom bubble. For example here's what the user will see in Safari and IE8.


In addition to polyfilling, Webshims also provides solutions for many of the limitations of constraint validation discussed earlier.
- Declaratively specify error messages through a
data-errormessage
attribute. - Provides classes
form-ui-valid
andform-ui-invalid
which work similarly to:-moz-ui-valid
and:-moz-ui-invalid
. - Provides custom events
firstinvalid
,lastinvalid
,changedvalid
, andchangedinvalid
. - Includes workarounds for WebKit false positives on form submission.
For more information on what Webshims provides refer to its HTML5 forms docs.
H5F #
H5F is a lightweight, dependency free polyfill that implements the full constraint validation API as well as a number of the new attributes.
To see how to use H5F let's add it to our basic example of a form with a single required
text input.
<script src="H5F.js"></script>
<form>
<input type="text" required value="" />
<input type="submit" value="Submit" />
</form>
<script>
H5F.setup(document.getElementsByTagName('form'));
</script>
H5F will prevent the submission of the form in all browsers, but the user will only see an inline validation bubble in browsers that support it natively. Since H5F polyfills the full constraint validation API you can use it to implement your own UI to do whatever you'd like.
If you're looking for some examples of how to leverage the API, H5F's demo page has an implementation that shows the messages in a bubble on the right hand side of the inputs. I also have an example that shows how you can show all error messages in a list at the top of forms.
In addition to polyfilling the constraint validation API, H5F also provides classes to mimic :-moz-ui-valid
and :-moz-ui-invalid
. By default these classes are valid
and error
although they can be customized by passing in a second parameter to H5F.setup
.
H5F.setup(document.getElementById('foo'), {
validClass: 'valid',
invalidClass: 'invalid'
});
For more information on H5F check it out on Github.
Conclusion #
HTML5's constraint validation APIs make adding client side validation to forms quick while providing a JavaScript API and CSS hooks for customization.
While there are still some issues with the implementations and old browsers to deal with, with a good polyfill or server-side fallback you can utilize these APIs in your forms today.