Native form validation 2: HTML and JavaScript

In this second part of a three-part article we will continue our study of native form validation in browsers. Part 1 discussed general UI considerations and CSS. Part 3 will discuss the native error messages and offer general recommendations to come to actually usable native form validation.

In this part we’re going to take a look at a few HTML features and the JavaScript API.

((This article was originally published on Samsung Internet’s Medium channel. Since I do not believe Medium will survive in the long run I re-publish it here.)

As usual in my articles, I’m quite vague about exact browser compatibility patterns because I already collated that information in the inevitable compatibility table. You can find the gory details there.

HTML attributes

HTML supports many potentially useful input types and attributes. I did the basic research a while ago, and while some details will have changed, the overall picture is still that most browsers support most features decently.

Here I want to draw attention to two features missing from my old overview: how the title attribute affects error messages, and the novalidate attribute.


It’s simple, really. The content of the title attribute of a field is added to the field’s error message only if the field has a pattern. This is useful for giving clues about the exact nature of the pattern; something that is impossible for the browser to determine.

It would also be useful to use the title for giving clues about the exact nature of fields that do not have a pattern, but, as we’ll see throughout this article, we can’t have nice things because that would make things nice for us. And we’re born to suffer. So title only works on pattern.


The novalidate attribute of forms works in most browsers. When present, the attribute tells the browser not to attempt any native validation. In addition to suppressing the native error messages it also suppresses all the rest of validation, so the form is submitted unless an old-fashioned form validation script that you wrote yourself prevents it.

If you want to retain part of native validation, but not the error messages, you have to use the invalid event, which will be explained in part 3.

The Constraint Validation API

Let’s turn to the JavaScript side of things. We will find an entirely different set of problems than in CSS that preclude useful form validation for entirely different reasons.

The Constraint Validation API is part of the HTML5 specification and that doesn’t really do a lot of useful things. (Gem: a form field value can be “suffering from being missing.”) Browsers support this API fairly well, with only one method lacking in older browsers. Unfortunately this is exactly the best-designed and most useful method.

Also, the creators of this spec did not pay any attention to what the CSS people were doing with :invalid. Here’s an example:

As we saw in part 1, fieldset:invalid works in most browsers and kicks in when at least one form field in the fieldset is invalid. The API allows us to use the checkValidity() method on fieldsets as well, but it returns true, even when the fieldset contains an invalid form field. (To make matters more complicated, several Chromia, but not the latest Google Chrome itself, implement checkValidity() on fieldsets correctly.)

Right hand, meet left hand. The two of you should connect one of these days.


But anyway. Let’s start with an API feature that actually works. Every form field has a validity property that contains a bunch of information about its invalidity. All browsers support nearly all properties, even though only a few are actually useful.

All properties come in the form formField.validity.propertyName. They are best summarised in table form:

Property Applies to is true when
badInput number the value is not a number
patternMismatch pattern the value does not conform to the pattern
rangeOverflow number the value is higher than the max attribute
rangeUnderflow number the value is lower than the min attribute
stepMismatch number the value does not conform to the step attribute
tooLong maxlength the user has attempted to add a character to a form field with a too-long default value
tooShort minlength the user has entered a character in the field, but there are fewer characters than the minlength value
typeMismatch email or URL the value is not an email address or a URL
valid any the field is valid
valueMissing required the field is empty

The properties that deal with number fields are useful: we can figure out exactly what kind of error the user made, and adjust our error messages accordingly.

Unfortunately the other properties are rather pointless. If there’s an error in an email, url, required, or pattern field it’s immediately clear what the problem is. The extra properties of validity are not necessary.

It would be useful if we’d get more specific data, such as “user hasn’t entered an @ in this email field.” Native error messages in fact do so in some browsers, but the validity properties don’t.

At least these properties do not actively harm native form validation UX. You will start to appreciate such slight blessings before we’re done with the API.

The tooLong saga

And then there’s the tooLong saga. This part of my research took way too long because the browsers saw fit to implement maxlength and minlength in a way that’s entirely different from all other constraints. I see no reason not to share my pain with you.

Take the following form field, and note it has a default value. If we validate it straight away we get the validity.typeMismatch error we would expect:

<input type="URL" value="nonsense">

I did all my tests with this sort of wrong default values because it’s way faster than manually typing in values in five desktop browsers and twenty-five mobile browsers. That works absolutely fine, except with maxlength and minlength. Lo and behold, the following field is valid:

<input maxlength="5" value="nonsense">

No problem here, no errors to be thrown, and no, the value is certainly not too long, thanks so much for asking. Incidentally, this field also gets :valid styles.

Try it here for yourself:

It turns out that maxlength and minlength only elecit a response from CSS and the API if the user has actually changed the value of the form field. Although this is not a bad idea in itself, it is vastly different from all the other constraints, and that’s what makes it wrong. Obviously, this exception was necessary in order to make our lives as web developers more miserable.


Before we study the three methods the Constraint Validation API offers, it’s a good idea to quickly review what we would actually like to do:

  1. Show a native error message.
  2. Rewrite a native error message with site-specific copy.
  3. Find out if a field is valid or invalid, and, if invalid, what the problem is.

The validity properties already allow us to do #3. Nonetheless we are offered an extra method: checkValidity(). Personally I don’t see the need for it, especially since it does not tell us what is wrong with the field; it just returns true or false without further comment.

reportValidity() also checks a field’s validity, and if it is invalid the native error message is shown. This is a genuinely useful method. Unfortunately it’s also the worst-supported of the three: Edge and quite a few mobile browsers do not support it.

Finally how do we set the text of a native error message? That is the domain of setCustomValidity('string'). If you use it on a form field the error message becomes the content of the string. If you use an empty string as an argument it resets the error message to its default value. And if you use no argument? It gives an error. Obviously. Allowing an undefined argument to default to the empty string behaviour would be good design, and we’re all agreed this API should be as crappy as possible.

Setting the error message text is not the only thing this method does. If you use a string as an argument it also sets the form field’s validity to false; if you use the empty string the validity becomes true.

The problem here is that these two functionalities, while very useful of themselves, are combined in the same method. Setting the validity of a form field is a good idea; for instance, if it has a constraint other than the standard ones built into the browser. Being able to produce a custom error message is also a good idea. But these two quite different tasks should be the jobs of two different methods.

The current method forces us to jump through complicated hoops if we want to set the error message of a standard constraint, since we can only do so if the field in fact turns out to be invalid. It would become something like this:

var field = [the field we're checking];
if (!field.validity.valid) {
	field.setCustomValidity('custom error message');
} else {

This is only a few lines of code. The problem is that you should run this code for each individual field every time the form is being readied for validation. That, too, is not impossible, but it’s kludgy and annoying. Above all, it’s bad design.

Anyway, here are the three methods, warts and all, in useful table form:

Method return value action
checkValidity() boolean Checks validity of element
reportValidity() boolean Checks validity of element. If invalid, shows native error message.
setCustomValidity('error') none Sets validity of element to false and sets error message to argument.
setCustomValidity('') Sets validity of element to true and restores dedault error message.
setCustomValidity() Error! You didn’t think you could afford not to send an empty string as an argument, did you?

That concludes part 2. In part 3 we’ll discuss the native error messages, draw some conclusions, and create a list of recommendations for improvement — and boy, will that list be long!

This is the blog of Peter-Paul Koch, web developer, consultant, and trainer. You can also follow him on Twitter or Mastodon.
Atom RSS

If you like this blog, why not donate a little bit of money to help me pay my bills?