Forms that work. Every time. Everywhere.

Ryan Bethel’s avatar

by Ryan Bethel

complicated forms Photo by Kelly Sikkema on Unsplash

Progressive enhancement for HTML Forms

Forms are critical. They make the web a 2-way medium. If your site is essential to someone, you should make it as dependable as possible.

A good form is:

  1. easy to use (UX),
  2. for everyone (accessibility),
  3. every time, everywhere (performance and reliability).

The fastest way to make a form unreliable is to scrap HTML’s built-in <form> handling and build it all in JavaScript. This post will drill focus on making forms reliable with progressive enhancement.

Start with the tightest constraints

Progressive enhancement (PE) means building your site to do its job with only HTML and then improving it with JavaScript. The biggest critique of PE is that you have to build your site twice. Once the way you want with a powerful front-end framework. Then you try to replicate as much as possible with only HTML.

A better approach is to start with the most constrained version. Take it as far as possible (which may be further than you think). And then add what’s left with JavaScript. The two versions will be much closer, easier to maintain, and more reliable.

Constraints are good. They force creativity.


Forms are complex because the processes they represent are complex. The best approach is to break it down into smaller <form>'s. One thing per page is a design pattern that encourages putting one concept on each page of your site. This creates a virtuous cycle. As smaller, simpler pages load faster having more pages is not a problem. These smaller forms are also easier to build without JavaScript.

Another way to break it down is with multiple small forms on the same page. You can add, delete, and update todo items on a single page with a straightforward form for each action rather than one complicated form handling them all. The like button for your messaging app may be a form by itself. This approach seems counter to the one thing per page pattern, but they both untangle a complicated process into simple forms.

Don’t be too clever

Progressive enhancement constraints do encourage creativity, but I am often tempted to be too clever. I once wrote about how to build complex state machines with only HTML (CSS-tricks:HTML-state-machines). There are exceptions, but in most cases, avoid cleverness in favor of simplicity.

Save progress

To save progress on a form without submitting it you can use a modified “submit” button. This button uses the formaction attribute to submit the form to a different endpoint where the progress is saved. The formnovalidate attribute skips the validation check that might otherwise block submission because of missing required inputs. The response should load the page in the same state so the user can continue from there.

<form action="/thing" method="POST" >
  <input name="thing"/>
<button type="submit">Submit</button>
<button formaction="/save-it/thing" formnovalidate type="submit">Save</button>

Focus is an important consideration here. Since the page reloads (even though the state is restored), the focus will move back to the beginning of the form. You can set autofocus in the last completed box before the save so the user can continue from there. It depends on the situation.

This is a perfect opportunity to progressively enhance this feature. It works as-is, but you can use the FormData object and fetch to save in the background without reloading. That solves the loss of focus problem.

HTML built-in validation is actually good

Validation is one of the biggest reasons people reach for JavaScript for forms. There is a myth that HTML validation is too limited, too ugly, and impossible to style. I believed this until I actually tried it. Built-in validation will get you most of, if not all, the way there. You can mark inputs with common types like “email” or “URL” and use validation attributes like “required” that set up built-in validation. These validations are checked before the form is submitted. You can even specify a regex pattern for unique inputs. You style these inputs and messages using the :valid or :invalid (as well as others like :out-of-range) CSS selectors. MDN is a great resource for more details (check out form-validation and constraint-validation).

The example below shows how to add basic validation to an email field. The two attributes (type and required) set up the validation requirements. The placeholder=" " is used to detect if anything has been entered in the field. The corresponding style will show a red border if an invalid email is entered and the field is unfocused.

  border:solid 1px red;

<input required placeholder=" " type="email"/>

Server-side validation

So what if that’s not enough? For security reasons you should validate input on the server anyway. This can cover any validation that is difficult on the client. If errors are found the form is returned with error messages shown.

Check for uniqueness

its an original antique

Users want to know if their name or favorite handle is available before they decide to continue filling out the rest of the sign-up. You can let them check with a button where the formtarget is an <iframe>. They can check repeatedly without the page reloading and the response is shown in the iframe. The endpoint is changed with the formaction so the rest of the form data is not recorded as a submission.

<input name="username"/>
<iframe name="username-availability"></iframe>
<button formaction="/unique/username" formtarget="username-availability" formnovalidate type="submit">Check Username</button>

Then progressively enhance with JavaScript to hide the button and run the check in the background.

Validation enhancements with JavaScript

Now that you have a solid form that meets your requirements there are some improvements you can add with just a little JavaScript.

Built-in validation messages are not very pretty or customizable. It is possible to connect to the constraint validation API to override these messages and add your own.

Use OnBlur to trigger validation. If a required field is empty it is invalid from the start. But nobody want to load a form and immediately see a bunch of warnings. Ideally, you would trigger those warnings only after they have touched the field and removed focus with it still invalid. Without JS you have to simulate it with something like :not(:focus):invalid:not(:placeholder-shown) (shown previously), but if they clear their entry and move focus the warning goes away. With JavaScript you can trigger this behavior to only happen after the field has been blurred.

Wrap it in a web component - but skip the shadow DOM

wrap this puppy up

At we are big fans of Web Components. They are a fantastic tool for progressive enhancement, but they have the opposite reputation.

What most people call “Web Components” are a collection of specifications often used together. But you don’t need to use them all. One of those specifications is called the shadow DOM. It creates a new self-contained DOM inside an element for strong encapsulation. This comes at a cost. It breaks many useful platform features, including forms. So wrap your progressive enhancements in custom elements, skip the shadow DOM,¹ and your life will be much simpler.

Conditional Examples

Breaking up your form offloads conditional logic to the server. Conditional features of later sections are served as new pages based on the previous responses. But some of that logic can’t be offloaded.

Dependent Input

Radio buttons can be used if one input depends on another. An example for a contact preference form might gives a choice of email or cell phone. The following input changes based on that selection. UI options for this are discussed in Web Form Design by Luke Wroblewski. It can be built with tabs, radio buttons, or a select dropdown. For progressive enhancement both radio buttons and tabs (with hidden radio buttons) can be used with no JavaScript (using the :checked CSS pseudo-class). But the select dropdown can’t be built in the same way.

Below is a simple dependent input contact preference form with radio buttons that will upgrade to a select dropdown when JavaScript loads. Minimal styling is show to highlight how it works.

See the Pen drop-section-progressive-enhancement by Ryan Bethel (@rbethel) on CodePen.

The custom element wrapper gives a place to put all the progressive enhancement code. To see the no JavaScript version comment out the customElement.define at the end of the JavaScript and reload. The select dropdown is tied to a dummy <form> tag so it does not get subscribed to the primary form. The JavaScript version uses the dropdown option to set the hidden radio button’s checked state. This way the shape of the form data submitted stays consistent for both implementations.

Adding and removing fields

In many forms a user needs to add an unknown number of items (i.e. names) to a list. One solution is to send a long list of name inputs to cover what might be needed. Another solution is to ask the user how many they need and send the list of inputs on the following page. Neither are great options. A better way is to include enough inputs, but hide them until the user requests.

The CodePen example below shows an add/remove feature with no JavaScript. The user can add and remove lines until all the hidden inputs are used. Four lines are used in the example. When all available lines are added or removed the final “add more” is actually a request to the server to send more. This reloads the page with the previous state plus additional inputs as needed. Again, minimal styling is used to highlight how it works.

See the Pen Add/Remove Form by Ryan Bethel (@rbethel) on CodePen.


Progressive enhancement seems hard, but is often easier in retrospect. The results are easier to maintain and better for users. Hard to find a win-win-win like that every day. It does require a different approach at the start. Give it a try with your next project at


¹ There are some cases where the shadow DOM is the right tool for the job, but I don’t think it is right for most. This topic deserves its own blog post. Maybe we will write that one soon 😉.