After doing exhaustive research into modern CSS and JavaScript form validation, I present my conclusions in this series of articles. It will discuss HTML validation messages, the CSS :invalid
and :valid
pseudo-classes, and the Constraint Validation API that is supposed to make form validation easier but doesn’t really.
In this article we will attempt to validate a form in a user-friendly fashion entirely by using existing native HTML, CSS, and JavaScript features, writing a few very light custom scripts to pull some supposedly-easy strings in the Constraint Validation 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.)
We will fail miserably. We will find that serious design errors were made, and that it’s no wonder web developers don’t use any of these techniques. Specifically, modern form validation suffers from the following problems:
These problems are all the more odd since form validation is literally the oldest trick in the JavaScript book: when JavaScript was introduced in Netscape 2 it could basically only do form validation. We’ve had twenty years to get it right, but we didn’t.
This is a three-part article. This part will treat general UI considerations and CSS. Part 2 will talk about a few HTML features and the JavaScript API. Part 3 will study the native error messages and offer a recommendation for better native form validation.
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.
Oh, and one thing before we start: always validate your forms server-side, whatever you do on the client. If your script fails, and you have no fallback in place, unpleasant things could happen.
(OK, so one other thing before we start. Many thanks to Stéphanie Walter and Geoffrey Crofte for their extremely useful feedback to a draft of this article.)
Before we delve into the depths of APIs and pseudo-classes we should have a clear idea of what we’re trying to achieve. In theory it’s easy: we want to offer the user a good user experience, which amounts to clear, concise error messages that are properly timed and properly placed.
The clarity and conciseness of the error messages is partly up to us: we have to write the right copy and then add it to the correct form fields. (Warning: that last bit is very hard.) Positioning is clear: they should go in the close vicinity of the form field they apply to. As to timing, there are three options:
Which of these three is best? Christian Holst treats the UI and customer experience of form validation in detail. His most important recommendations are to show the error messages next to the fields they apply to, and to show them immediately when the user is done filling out the fields. In other words, the onblur timing is best.
Luke Wroblewski concurs, and adds the important observation that users are best served by persistent error messages, i.e. messages that don’t disappear after a while. (And guess what all browsers except for Firefox on Android do?) The same goes for success messages, by the way.
(Luke also tested a fourth timing option: focus + keypress, that shows messages when the user enters the form field and types. His tests showed conclusively that users find this a bad idea.)
Adrian Roselli adds one consideration: error messages should be shown above the form field, and not below, because on a tiny mobile phone screen a message below the field could be covered up by the software keyboard or other UI elements.
The counter-argument is that by now users have grown used to error messages next to or below the form field. Since the screen may not be wide enough, placement next to the field is a big no-no on mobile. Error messages below the form do not have that problem.
I will leave it to you to decide between these arguments, but will observe in passing that nearly all browsers place their error messages below the form field, though some mobile ones occasionally break that rule.
So although the error message placement is not entirely clear, the timing has to be onblur, i.e. when the user indicates she’s ready by moving away from a form field. Also, there should be an easy way to add custom error messages to individual form fields.
It will surprise no one that CSS, the API, and browsers do not implement these simple rules. It is not possible to add error messages in a simple, declarative way. The Constraint Validation API is based on onsubmit timing, while CSS :invalid
and :valid
are based on onkeypress timing.
Pure CSS form validation relies on the :invalid
and :valid
pseudo-classes. (There are other pseudo-classes, such as :out-of-range
, but we’re going to ignore them because they work the same, and are more specific instances of :invalid
.)
Try the pseudos here. The form field below only accepts “ppk” (lower case!) as a valid value. Any other value is invalid. However, the empty value is also valid, since this is not a required field.
As you will notice, validity is re-evaluated on every key stroke. This is certainly ugly and potentially confusing. The user does not need to know about the state of her value every step along the way; one crisp, clear message when she’s finished is quite enough. (Of course the user doesn’t actually see an error message yet. We’ll get back to that shortly.)
Fortunately it’s fairly easy to move to onblur timing by using a slightly more complex selector (thanks to Krijn for this trick):
input:invalid:not(:focus)
Now the invalid and valid styles are only visible when the field is not focused; i.e. when the user is not typing.
There. That’s much better.
That solves the timing problem. It does not solve the problem of showing error messages, though. Theoretically speaking, the following ought to be the solution:
input:invalid:not(:focus):before { content: attr(data-error-message); }
Show the content of an invalid field’s data-error-message
attribute just before the field if the user is not currently typing. Sounds great, right?
Alas alas, :before
and :after
are not allowed on replaced elements, of which <input>
is one.
I should amend that. :before
and :after
are not supposed to be allowed on replaced elements. However, in Chrome and Safari, :before
and :after
work on ranges, checkboxes, and radios. In Chrome and Safari/iOS, but not in Safari/Mac, they also work on the date-related types. (Why these exceptions? I have no effing clue. Probably some browser developers were drunk at the wrong time.)
So this doesn’t work. We have to move the error message outside the form field. Something like this:
span.errorMessage { display: none; } input:invalid:not(:focus) + span.errorMessage { display: block; }
This works, but the lack of easily declaraed error messages is disconcerting. You could argue that they don’t belong in CSS, but as we’ll see HTML and JavaScript don’t offer them, either. This is a fundamental problem with the specifications as they stand right now.
Although it might seem we’ve come a decent way and CSS-only form validation is within our grasp, it falls apart when we consider required form fields. A required field without a value is invalid. Like this:
Do you see what happened here? The field is already in the invalid state on page load. Telling the users they’ve made a mistake before they even had a chance to interact with the form is bad UX.
You might use this solution that, again, Krijn came up with (add a placeholder text of one space!), but it depends on the :placeholder-shown
pseudo.
:not(:focus):not(:placeholder-shown):invalid
Isn’t this getting a bit ridiculous? I mean, we’re not asking for arcane functionality that only a few form fields need. We just want to wait for the user to signal she’s ready before deciding if a form field is valid.
In other words, CSS-only form validation is not an option if you have required fields — and since most forms will have at least a few of them, CSS validation is not an option, period.
This is in fact the one problem that has been recognised by the CSS speccers. A solution is in the making in the form of the :user-invalid (W3C) or :user-error (WHATWG) pseudo-classes. Both would mean “if a form field is invalid after the user interacted with it,” which would solve most problems mentioned above.
As of this moment neither pseudo-class is supported in any browser. Below you see one more test input that has both pseudo-classes defined. Try it, and if the input gets a red (user-invalid
) or blue (user-error
) colour you know that it works.
It is to be assumed that they’ll be implemented eventually and will bring CSS form validation one step closer. The lack of a native, simple error message system will remain a serious problem, though.
It is not generally known that :invalid
and :valid
work on more than just <input>
. Fieldsets and form elements also allow these pseudo-classes, which evaluate to :invalid
if the fieldset or form contains at least one invalid field.
Even better, fieldsets allow :before
and :after
.
So this could be a useful technique:
fieldset:invalid { border-color: red; } fieldset:invalid:before { content: attr(data-error-message); }
Alas alas, still no way to easily declare error messages. Besides, Edge and quite a few older mobile browsers don’t support it.
Also, if fieldsets can be invalid, and if forms can be invalid, why can’t labels be invalid? Well, because they can’t. Obviously. Browsers don’t support it. We cannot have nice things because that would make things nice. And we cannot have nice things.
That concludes part 1. Next time we’ll talk about a few HTML options and the JavaScript API.
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?
Categories: