I recently embarked on improving the client-side form validation for a client. There were about 400 lines of form validation code stuffed inside a 1000 line form_helper.js. I looked for lightweight form validation scripts but after some hemming and hawing I decided to try my hand (again) at native HTML5 Form Validation.

If you’ve ever experimented with HTML5 Form Validation, you’ve probably been disappointed. The out-of-box experience isn’t quite what you want. Adding the required attribute to inputs works wonderfully. However the styling portion with input:invalid sorta sucks because empty inputs are trigger the :invalid state, even before the user has interacted with the page.

I finally sat down and spent a couple days trying to make HTML5 Form Validation work the way I want it. I had the following goals:

  1. Leverage browser-level feedback, free focus management and accessible labelling
  2. Only validate inputs on submit
  3. Styling with .error class

With this wishlist in hand, I set off and found a solution that works with only 6 lines of code.

The Setup

First we create a basic HTML5 form with a required attributes on inputs we want to validate…

<form action="/users/signup" method="post">
    <label for="email">Email</label>
    <input id="email" type="email" required>
    ...
    <input type="submit" value="Submit">
</form>

And then we need the CSS for the error state…

input.error {
  border-color: red;
}

Again, we want to use a CSS class and not the input:invalid pseudo-class because even before the user interacts with the form on the page, input:invalid makes it look like the user made an error. This is a bad UX. Never blame the user.

😱 Form Validation in 6 Lines of JS!

I instinctually reached for the submit event to try and manipulate error handling. However, this won’t work because the invalid event from the inputs block the form’s submit event from firing. It’s good that our form won’t try to submit invalid data I wasn’t sure how to validate a form that was never submitted. I tried a few grossly complex workarounds.

Eventually, I discovered the simplest solution by hooking into the input’s invalid event. Just before the submit event, the browser performs a form.checkValidity() check which checks all the inputs. All inputs with invalid data fire the invalid event to say “Hey, I have invalid data!” It’s here that we can apply the error class we need.

const inputs = document.querySelectorAll("input, select, textarea");

inputs.forEach(input => {
  input.addEventListener(
    "invalid",
    event => {
      input.classList.add("error");
    },
    false
  );
});

🎉 We did it! Now when a form is submitted, the invalid event fires on invalid inputs, and we add an .error class which adds a red border around the input. And we’re done! 172b. Form validation that fits in a tweet.

♿ The best part is we’re leveraging the browser’s focus management and accessibility behaviors. When a form submission is prevented, the first :invalid input gets focused and screen readers read out the error message for that input. This is great. Focus management isn’t fun to code.

💪 We’re also leveraging HTML5 input types for common pattern matching. input[type="email"] expects an email address and the error messaging will say something along those lines. Same with input[type="url"], input[type="number"], etc. HTML input types are basic but powerful stuff.

But wait there’s more complex things we can do!

Optional: Validate on Blur

If you like your forms to immediately flag errors after leaving each input (which that’s fine, I’m not judging), it’s pretty easy to add that functionality. In fact, it’s just an additional 3 lines of code…

input.addEventListener("blur", function() {
  input.checkValidity();
});

This On :invalid inputs, it will trigger the invalid event and add the .error class we programmed above.

Optional: Validate Regex Patterns

I thought this was going to be awful and ruin my simple solution. Custom error messaging could be gnarly as well. But NOPE. HTML gods spared me. Behold, regex pattern matching in ZERO lines of JavaScript!

By using a title attribute to describe the expected data input and a pattern attribute with a regex pattern, we can pretty much validate against anything we want.

<label for="alpha">Alphanumeric Only</label>
<input id="alpha" type="text" pattern="[a-zA-Z0-9]+" title="Only alphanumeric characters allowed" required/>

There’s even a website, HTML5Pattern.com, that has the regex for almost every kind of validation you’d ever want to create.

Demo: Putting it all together

Here’s a Codepen of a complete working form with Pattern Matching using this approach.

See the Pen Native form validation onsubmit with pattern?? by Dave Rupert (@davatron5000) on CodePen.

Tradeoffs

Every solution is a system of tradeoffs. The major tradeoff we’re making here is that in leiu of controlling the styling of client-side validation errors, we’re leveraging the browsers native tooltip-style validation messages. It doesn’t look the coolest, but it works really well for casual client-side validation.

Instead of being perscriptive about error messaging, we use what the browser natively gives us. We’re of course doing server-side validation and are free to style those errors however we want (usually that’s a more critical error and needs a better UX).

If this is unacceptable, Hyperform.js is a pretty small (32kb minfied / 9kb gzipped) reimplementation of the same HTML5 Form Validation API in JavaScript, but with additions to iron out some weird parts.

Let me know if you try this and have any problems with the approach. I’ll probably use this as a starting point for every project ever. Happy coding!