Building Super Powered HTML Forms with JavaScript

A talk on using JavaScript to progressively enhance forms to improve user experience without negatively impacting native functionality.

I recently gave a talk for the Conf42: JavaScript 2021 conference on using JavaScript to progressively enhance HTML forms. The goal was to show people how to improve user experience without negatively impacting native functionality.


Today I’m going to be discussing adding JavaScript to HTML forms to give them superpowers. Before we get too far, I want to explain what I consider to be superpowers. These are going to be user experience improvements that do not negatively impact functionality, native functionality, accessibility, semantics performance or security. This is also commonly referred to as progressive enhancement. So getting to JavaScript, the first rule of JavaScript is knowing when to use it, and therefore knowing when not to use it. JavaScript itself has some inherent cost when we decide whether or not to add it to a page. So HTML and CSS are generally going to be faster except for special occasions. And at the point that we decide to start adding JavaScript, we may start incurring things like extra HTTP HTTP requests more data to download, we might have JavaScript that blocks some rendering performance, or rendering time. JavaScript has a runtime performance issues, we may have JavaScript that has unexpected errors or exceptions that prevent the rest of the script from running. And it’s possible that JavaScript may be disabled or blocked by a user. Now, HTML on the other hand, natively gives us a lot. We have some concept of state management in the input values. We have clickable labels without needing to add any sort of JavaScript to click something and make a label focused. We have accessibility built in for keyboard navigation focus states screen reader support. We have consistent experience across browsers and devices if the user uses a radio or checkbox in one browser and should behave the same in another. This also great for users that prefer using the keyboard to fill out forms. And we have benefits for things like implicit submission, which we can discuss later. Lastly, that’s going to be a recurring theme is validation. Validation built into HTML costs us zero, and it has quite a good number of resources available to us. Another benefit of the built in validation attributes is they could hint to assistive technology, things like whether an input is required. Now between or when we’re building HTML forms are really looking at two different things. The inputs are the controls, and the form itself. We’ll start by looking at the individual inputs built into the browser, we have 24 Different options if you account for all of the input types, text areas, selects, etc. So when I browse the internet, I still wonder why I see things like this a div that’s designed to look like a checkbox and has some JavaScript event listener says I’m a checkbox. One of the reasons we still see this probably has to do with styling in 2019, Greg Whitworth did a survey, he asked, he got 14,000 1400 respondents and have them the most common reason people create their own native controls, or recreate native controls is for styling with the number one control that they recreate being the select, you can see more at this URL slides will be available. In that discussion, he points out that the amount of work it takes to implement an accessible alternative with complete feature parity is massive. And here’s an example of that. If we want to take that previous checkbox and make it a little bit more on par with what we get built in, we can see that we have a class to add some styling to make it look like a checkbox, some ARIA attributes for assistive technology. We have some tab index for key keyboard navigation. And we have two event listeners one for clicks with the mouse one for key key downs. Compare that to the native solution, which is an input with a label looks like more work to me. And this isn’t even considering the JavaScript that goes into those event handlers. And it’s not the most complex form component to recreate like radios or select. So the good news is when it comes to styling, it’s better than it used to be. CSS gives us a lot of pseudo classes to work with based on the state of the input. We also have features like appearance, none and pseudo elements that let us create custom checkboxes and radios for example, we can use tricks like visually hidden input, and then target a sibling selector to make something look cool based on what that input is doing. And there’s more information on my blog. Again, the links will be available in the slides if you need.

Here are a couple examples of some totally custom looking UI form components. All of these can be accomplished with just HTML and CSS, no need for JavaScript for things like this. The other good news is that the future is bright. There’s people discussing, potentially bringing things like new pseudo selectors and parts so that we can style things like the Select drop down, which is a complex component. More specifically, and potentially things like name slots. So if we want, we could take those selects, and be able to customize the individual aspects of it by providing our own markup. This is just an example, which I guess you can do with emojis, but you get the idea. There’s more details about these things that may be coming down the line if you go to open, a lot of discussion going on there as well. The there’s some editorial proposals for select checkbox and file as of today, there’s other editorial proposals, but these are the ones that relate to forms. Now, you may be wondering yourself, I thought this was a JavaScript conference, what’s the deal. And the good news is that JavaScript is actually really good for taking what we have natively and improving upon it without breaking something, we can take that native form validation that we were discussing, and we can customize the message or manually trigger it. We can also do things that HTML alone cannot do such as toggling ARIA well for accessibility, toggling aria of invalid or ARIA disabled attributes, we can provide improved user experiences by doing things like an input, that’s a password input that you can toggle whether the visibility of the password or a text area that automatically expands and shrinks based on how much content is in there, or phone input that does the masking and formatting to show that it’s a phone number. Of course, you want to do all this in a way that doesn’t detract from the user experience. And lastly, getting back to the conversation of validation, we may want to provide our own custom UI for validation messages. So looking into that the browser has built into it without anything else that we need to reach out for a third party library, the validity State web API, which is one of my favorites. If you have an input DOM node, it’s right on the validity property. And it gives you an object with all of these different properties that are true or false based on whether those HTML the corresponding HTML attributes are valid or not. We can use this API to do things like toggle the ARIA invalid state, or we can add maybe like a div or list of error messages and associate that to the input with ARIA described by and give it a live region so that assistive technology users are updated. So with the inputs kind of covered, now we can transition over to forms. Now, forms are going to be a little bit different, because forms don’t have a built in UI that you have to deal with. And so some people omit them all together and will do something like an input that you just press the Enter key on and it does something. Now I would argue that almost every input would actually benefit from having a form HTML tag, wrap around it, because it can give us some additional features without with actually doing less work. Things like that native validation that we just talked about. That only works if we have an input inside of a form element, the implicit return which is when we compared to a JavaScript event listener that looks for the keydown event and checks if it’s the enter key and then does a fetch request, we can get all that built in by just putting a text input and a form when you hit the enter key, it’s going to submit the form. We can simplify the JavaScript API requests by using a form tag. So if we want to send Ajax requests, we can actually make it easier on ourselves by using an HTML form. We also get resiliency, again, going back to that idea that JavaScript may experiencing an error and you probably want to fall back to HTML to submit forms if your AJAX request is not available, right.

So assuming that we’re using forms, we can do the same thing that we did with inputs where we can take JavaScript and enhance upon the native experience because some people don’t want to submit all their forms when The native experience, which is a page refresh. So some things that we can do that are, in addition to the native experience might be keyboard shortcuts like Ctrl, enter to submit the form when you’re focused on a text area, which is not possible with just HTML, we can have repeater input fields. So think of a collection of a few inputs that you can make multiple copies of thinking, if you have like an ecommerce site, and you want to have a product, name, price and picture and you want to add many of them at the same time. Drag and drop is a common thing that we can do with JavaScript that we can’t do with HTML. And maybe that lives within a form somewhere, I don’t know. Then we can have again, the custom validation you user experience if you don’t want to rely on the native validation. Because looking at the data validation, this is what it looks like, we might try and submit this form, it’s a required field and we get a pop up that says, Please fill out this field. And data validation is actually quite useful in terms of the features that it provides. One thing is when we submit an invalid form, it focuses on the first Invalid input, which brings our focus there. As a added benefit of the focus going there, we will actually also scroll there, the browser will scroll to that input. So if if the form is long enough to take up greater than the screen of the browser, and you hit that some more that submit button, you want to scroll to the input that is invalid. And naturally, or obviously, it explains to the user what the error is with the form or with the input. Now there’s just one problem with the native UI. And that is that there’s not really a good way to customize it. So if we care about branding, we may want to be able to do that. And I think about this is why not supporting both, why not take the native HTML, validation constraints and tap into those using JavaScript to enhance upon it. This way, if JavaScript does get disabled, our form validation logic will still work because it falls back to the native HTML one. There’s also less to learn because compared to a third party library for validation, we don’t have to learn what their API is, we just learned what’s native to the browser. And if we, and there’s no need to, you know, if you learn one library and then decide to move on to the next one, they have a completely different API, right. Compared to libraries, again, we probably will have less to download as well, which would improve performance also make maintenance easier, because we don’t have some third party dependency that we have to keep up to date. And it could potentially improve security by not having to deal with NPM vulnerabilities that, you know, intentional or unintentional. Now, the last point is that validation logic really doesn’t belong entirely on the front end, you don’t want to rely, you don’t want the business logic of your application relying on client side validation, because it’s possible to make form submissions outside of the front end anyway. And so you’re going to be doing validation on the back end. So there’s really not a need to have a whole robust solution on the front end, just something that improves the user experience, but doesn’t necessarily need to be super robust. There are occasions when you might still consider a third party library, I’m not saying you shouldn’t use them. But I like to start with the native things and enhance on it a little bit before I need to reach for third party. Now looking at rolling our own sort of custom validation experience, the first thing we want to do is prevent the default native validation UI from occurring. The way we can do that is by adding the no validate property onto our form, so that it tells the browser Hey, don’t bother validating this. And we want to do that with JavaScript because that means JavaScript is enabled.

Next, we want to check on to we want to listen to a submit event, and we want to scroll to that first input. The first Invalid input, we can do that by onsubmit. Checking if the forms if the form is valid or invalid, we can do that with the check validity method on the form DOM node, it returns a Boolean. And if the form is not valid, then we can do a query selector for the first invalid input and focus on it that’s also going to maintain parody. with native HTML experience where it focuses on it, and because it’s focused, it will scroll to it as well. If it’s invalid, we can return early. If it’s not invalid, we can go ahead and do the submission or the logic to submit our form, maybe with a fetch request, we also want to prevent the default behavior, which would be the browser refreshing and sending that request. Next, we can look at what does that API request look like or sending that fetch request, I want to look at this in a way that maintains feature parity. And we can actually use this on whatever form we want, we don’t need to have a specific fetch event for you know, the login form versus the Register Form versus whatever form can use the same thing on all of them. So this function might look like this. We’ll start by listening to a form submission event, we’ll grab the form DOM node out of the event target, we’ll start building out our fetch parameters based on the form attributes. So we’ll get the URL from the action, we’ll get the form method from the form method, then we’ll capture the data from our inputs in that form using the form data web API. We don’t need to do anything more to capture the information. As long as our inputs are semantically written and have a name and everything. We can also capture data in the form of a URL search params, which will make more sense in a moment. We want to do a check whether this forms encoding type is multi part form data, if that doesn’t make sense to you, it basically kind of comes down to whether we’re sending a file or not. But it’s not the default encoding type. So we’ll get to that in a moment as well. Next, the forms can have a get request can send a GET request or a post request. So we want to check whether it’s a get request, if it’s a get request, we want to send our data by means of URL search string parameters. So we’ll take the URL that we had, and we’ll append on to it the query string parameters for that, that payload, think I actually missed the little question mark there, but that’s alright. If it’s not a get request, we know is supposed to be a post request, which means we’ll put our payload in the body of the request. And we can check whether it’s a multipart form data. If it is we can send it with the form data object. If it’s not, we can send it with the URL search parameters object. At the very end, we want to prevent the default behavior because we, we want to make sure that everything before this line has completed successfully before we prevent the form from submitting using the native API, the native HTML submission. And at this point, we know that it’s everything’s all good. So we can send that fetch request with the URL and the methods that we want or the options that we defined. There are a couple of caveats to sending form submissions this way. One is if we’re dealing with HTML, if we’re pulling the the methods from the HTML form, we really only have the GET and POST method available, which if you don’t control your API endpoints, that might be an issue. This also probably means you want to detect whether the form or whether the data is being sent to your back end through JavaScript or through a native form submission. The reason being, if you send it through JavaScript, it might be safe to expect a JSON payload as the response. However, if you send it with HTML, because it’s going to reload the page, you probably don’t want to reload the page with a JSON response, you probably want to, I don’t know, maybe redirect the user back to the page that they came from, so that, essentially, the page refreshes. And they’re none the wiser. This also doesn’t work if you’re dealing with complex data types. So if you’re dealing with nested objects, or an array of objects or things like that, you just can’t do that with HTML forms. You also can’t send Graph QL requests, because that needs a special sort of formatting.

And moving on from feature using JavaScript to achieve feature parity in terms of validation and form submissions for better user experience, we can add additional features such as preventing data loss. So if a user’s filling out a long form, it might be really annoying for them to accidentally navigate away or refresh. And we can help them by having a little pop up that checks. Hey, are you sure you want to leave the page right now? The way we can do that is by tapping into the before unload event on the page. So we can do that with The window add EventListener to before unload, and then do a check whether the user has made any changes or not. If they have not mess with the form at all, it’s probably okay for them to refresh the page. So we can just return early and let we don’t even need to show them this little message. If they have made changes to the form, we can trigger this message, we can’t customize it. But we can trigger it by doing the prevent preventing the default behavior on that before onload event. We also need another line for Chrome for whatever reason. But this is a nice little user experience improvement to make your users life better, because then they don’t lose the data that they’ve spent so much time working on. There’s a more information on how to do this on my blog as well, if you want to get the slide presentations, check that out. In addition to preventing data loss, we can do things like keeping backups of the data that they have, of course, you don’t want to do this for very sensitive data. But let’s say we’re not dealing with sensitive data that that’s okay to, to share. What we can do is check whether the user has made any change to any of the inputs on the page. And every time that they make a change, we can capture the data from the form. So the inputs and their values is like key value pairs, we can put that into a JSON object and then Stringify it and store that in local storage. And then later on, if they leave and come back when the browser loads or when that form lands on the page, we can check local storage, pull that see if that data exists in local storage. If it is and we have an object, we can loop through the properties and values of that object and assign those values to their corresponding inputs within the forum. That logic is a little bit too long for me to put here. So just imagine it was a really awesome looking code. Finally, when that form is submitted, we want to clear out local storage so that when they come back, they’re not looking at data that they’ve already submitted. Just an example. So those are things that we could do to take native HTML and use JavaScript to build on top of it and give it sort of these super cool improvements without sacrificing accessibility or, you know resiliency. Now, I want to take a moment to look at component frameworks because I think this is where the the real superpowers get unlocked. The benefit of using component frameworks such as react or view means that it simplifies, or we can simplify the form creation process, we’ll look at that. I’ll explain what that means in a minute. But essentially, as a developer consuming some of our components, it’s, it’s not as much work to do all of the markup and IDs and labels and ARIA attributes and event listeners and all this stuff just much simpler. Next, we get repeatable quality. So when we spend so much time working on forms, we want to make sure that they’re well built. And they’re they work for everyone. And they work across devices and browsers and all that. So by having component frameworks we can actually implement that can put that same component in multiple places, and we get the same quality over and over. It also makes maintenance easier. As we’ve implemented that component over and over and over, we may discover that there’s actually a bug in that component. And rather than having to go throughout a site and fix every instance, that there ever was an input or a form, we can make that fix in one place, and have that fix permeate throughout our application. So that every input suddenly is fixed, or every form.

We can also do things like enforcing best practices. So react in view, I can speak to I have experience with and they provide methods for you to require certain things when you implement things like like an input. So saying that every input requires a label or a name or things like that. You can enforce. There’s also things that we know are required for every input such as IDs. But we don’t have to be as strict with we can we can generate those and have kind of a fallback for developers. So let’s look in a view example of what that might look like, is I have a component here where I’ve defined, you know, here, just the props. And with these props, I can say that we have a label, and we have a name prop that this component expects. And I can say that both of those are required, as the developer creating the component. I have no idea how this input is going to be used. But as the developer consuming it, you’re going to be required to give me those fields because for a, for a fully accessible and quality input, I need them. I also need the ID of an input in order to maintain those ARIA attributes. But I don’t necessarily need you to give me an ID, you can, you may if you want, and I’ll take it, otherwise, I can fall back to generating randomly created one. Now this input, we may want to add validation logic to anytime that experiences a blur event. So we and then that validation logic, we want to show some errors for so we can start with some reactive properties of tracking error, an array of errors. And then on that blur event, we can tap into the validity state of that input and check whether it’s invalid or not. For each of those properties that we saw before, we can loop over them, see what the property check what the property is check the whether it’s valid or invalid. If it’s valid, we can move on to the next property, if it’s invalid. In this example, we’re looking at the range underflow property which corresponds to the min attribute. So if the min attribute is invalid, we can push to our error object, hey, this, this input must be greater than whatever the minimum attribute is. And we can push that error to our reactive error array, and then present that in the UI. So looking at the UI for this component, it might look something like this, we might have the label that’s associated to the input through that ID, we might check whether this is a required a required input or not based on the attributes, and if so maybe put a little red asterisk next to the label. We’ll have our input, of course that has our validation event handler, and the ID and everything else, an aria described by. And then we might have our UI for showing those error messages. And that can be associated with the input through the ARIA described by Attribute generated with the ID or, you know, based off of the ID, and has a role of alert. There’s a little bit more this is this example is inspired from a an input component in the utensils library, which you can check out later. Now besides the input, we can also create a component for our form. And our form component might have a submit event handler that checks the validity of the form kind of like we saw before using the check validity method. And if the form is invalid, we can, you know, automatically focus and scroll to the first invalid input. But then, in addition to the same features that we’ve discussed, and kind of adding those to a component, we have new features available, which are custom event emitters. And this will make more sense in a moment. But we can base basically create custom events for when the form is invalidly submitted. And when the form is validly submitted. This components markup is a lot simpler. It’s just a form, it falls back to a POST method for security reasons, which I don’t have time to get into now. It implements a slot in React, this corresponds to the component children. To make life easier for all the development developers, maybe it includes a submit button, which is not customizable in this case. But you can imagine.

And so putting all of this together, once we have this component logic, we have a couple of components that make life a lot easier have robust functionality, and user experience improvements. And the implementation details are actually very simple. So as the developer now that’s implementing these, I might create a on valid submit handler and an on invalid submit handler. And on valid submit, I want to send that information using the JavaScript fetch function that we defined earlier on an invalid submit. I’m pretty, I don’t do things right. So I’m just using console log, because whatever. And then when we actually implement our form, we define the action where that where we want the form to submit things to, on a valid submission. We use the JavaScript submit handler on an invalid submission, we just console log it. Within that forum, we have two inputs, one for the email, one for the password, and you can see that this markup really simplifies What our forms could actually look like. So it’s a very nice user experience or developer experience. For me, it’s a very good user experience for the end user, because they get all of the quality that I’ve put into the input components. At the end of everything, JavaScript is really awesome for forms because, well, in my opinion, in my opinion, when we use progressive enhancement, because one is by relying on the native UI or the native elements, we get a consistent experience across all browsers and all devices. When you see a checkbox in one place, you and you can see the same checkbox in the other place, you know how to use it. Number two, it’s accessible for everyone. So able bodied users, visual users, people that prefer that prefer keyboards, people that are reliant on assistive technology, everyone can use your application, which is great. We have minimal performance impact when compared to either only using JavaScript to build out those custom form controls that we discussed earlier, and having to add all of the sort of logic in order to have feature parity and accessibility and everything. And when you enhance it with JavaScript, it works with JavaScript. But when you build it in a way to fall back to HTML, it also works in case JavaScript is disabled, there’s an ad blocker somewhere. You’re script has an error in it for whatever reason. I know that I experienced one time that I actually tried to sign up for an application. And they use Java, they relied on JavaScript to sign users up. And because I had a, like an ad blocker or a tracking blocker or something like that, the application didn’t work. So as a result, I couldn’t even use the application. And you know, I don’t think I don’t personally want to lose out on users that experience something like that. If it falls back to HTML, it’s great, it still works. And lastly, when we use component frameworks, we really get the benefit of being able to compartment, compartmentalize all of the logic, all of the quality, all of the user experience improvements into one place that we can use over and over throughout our application. Put that quality over and over throughout our application and also simplify the maintenance. That’s the end of my my talk today. I hope you enjoyed it. If you want more I wrote series I spent like a year writing a series on all of the things that I think make building HTML forms good. I also maintain the view tensile Vue js library that includes the custom input and form controls, as well as a whole bunch of other things. I read a loose newsletter, and a blog if you want more content like this, or you can follow me on Twitter. And probably the main reason to be here today is I have a really cute dog. His name is nugget. He’s a chiweenie is 11 pounds, loves chasing squirrels and food and you should give him a follow. So thank you very much for your time and paying attention and I hope that this talk was worth it.

Thank you so much for reading. If you liked this article, please share it. It's one of the best ways to support me. You can also sign up for my newsletter or follow me on Twitter if you want to know when new articles are published.

Originally published on