HTML Blog cover

How to Build HTML Forms Right: Semantics

Forms are arguably the most important parts of any web application. Without forms, we would not have sites like Google, Facebook, Amazon, Reddit, etc. However, the more I browse the web, the more I see poor implementations of forms.

In this series, we will examine the proper steps to creating forms for the web, how to think about the code we write, and considerations to make along the way. The series is broken up into the following parts:

  • Part 1: Semantics
  • Part 2: Accessibility
  • Part 3: Custom Styles
  • Part 4: User Experience
  • Part 5: Security

Starting with proper semantics makes the most sense because, as we will see through the series, semantics will play a role in every other section. It’s also most likely the starting point when you begin coding. So let’s get started

Start With A Form

First and foremost, any time we are working with some sort of user input, we should begin with a <form> tag. Some folks working with JavaScript may choose to omit the <form> tag because the form submission can be handled with an AJAX request, but this is a mistake. Not the AJAX part, but leaving out the <form> tag.

The <form> tag is important for accessibility reasons. Some screen readers have a mode specifically for forms which is only activated when inside a <form> tag.

Including the <form> tag also ensures that our form can be submitted even if JavaScript is not enabled. You just want to make sure to include the forms action attribute so it knows where to send the data. Hooray, progressive enhancement!

To this, you may respond, “But Austin, I’m building an SPA. Therefore if the user even sees the form, it means JavaScript MUST be enabled.” And you’d be right. Although, if it is an important form, you may want to consider supporting a no-JS world. The day may come that you want to implement SSR. In which case, see the previous point about progressive enhancement.

If you’re a JavaScript developer and you still aren’t convinced by the previous points, consider the following code. Look how using a <form> makes our work easier. By using the form’s submit event, we can access the form Node, which makes sending data much easier thanks to FormData.

const form = document.querySelector("#your-form")
const url = "https://jsonplaceholder.typicode.com/posts"

form.addEventListener('submit', e => {
  e.preventDefault()

  const data = new FormData(e.target)
 
  fetch(url, {
    method: 'POST',
    body: new URLSearchParams(data)
  })
})

(Note that if you can send FormData directly as the request body, but it will set the HTTP headers to multipart/form-data, which is not usually what I want).

There is also a user experience consideration for using a <form> tag. Have you ever filled out an input, pressed the “Enter” key, and the form magically submitted even though you did not click a submit button (for example, a DuckDuckGo search)? This beautiful user experience is possible because the <form> tag is present.

(I guess you could use JavaScript to find every input in a form and add a keydown event listener for the “Enter” key, but isn’t it easier to just use a <form>?)

Lastly, the <form> tag has no visual properties. So I can’t think of any good argument to leave it out. Less code, maybe…? But the benefits of using it far outweigh the costs.

Use The Correct Inputs

Using the right input types is critical when building a form. Sure, a <div> is easier to style in many cases, but there’s so much that comes baked into native form elements that you would have to recreate:

  • reachable via keyboard navigation with the tab key
  • focusable
  • access to client-side validation
  • keyboard interactions to choose radio inputs
  • keyboard interactions to check checkboxes inputs
  • keyboard interactions to choose select options
  • implicit form submit
  • descriptive semantics for screen-readers

Try to do all that with a <div> and you will likely fail miserably and end up quitting web development. Please don’t do that.

Instead, you should be able to find an appropriate input for just about anything you can think of. After all, according to MDN, there are 22 different types of inputs, as well as <select> and <textarea>.

Need some help?

  • Is the input a selection of options?
    • Yes: Are there more than 6 options available?
      • Yes: Can more than one option be selected?
        • Yes: Use a <select> with the multiple attribute
        • No: Use a <select>
      • No: Can more than one option be selected?
        • Yes: Use several <input type="checkbox">
        • No: Use several <input type="radio"> with the same name attribute
    • No: Is it a long piece of text?
      • Yes: Use a <textarea> element.
      • No: Does the input have an appropriate type?
        • Yes: Use the proper input type
        • No: Use a normal <input>

At this point, you may be saying, “Yes, that’s all fine and dandy. In fact, I already knew about all the different input types. But I was given a design with super fancy inputs that don’t look like any of the inputs available.” Well, tough. For a long time, we had to choose between custom designed forms, or semantic forms.

That’s no longer the case, and we now have plenty of flexibility for form design. We will look deeper into this later, but for now, just trust me. Start developing with a solid, semantic foundation and put the custom design on top.

One last thing I want to mention regarding semantics is that in any time you have multiple inputs that are related, they should be wrapped with a <fieldset> tag. One example could be two fields asking for a first name and last name.

The most relevant use case for the <fieldset> is for radio or checkbox inputs. Almost always they are grouped together. The inputs themselves each have a label, and the group itself also needs a “label”, but when working with a <fieldset>, we use the <legend> tag.

Every Input Gets a Name and a Label

The name attribute is required for any form input to send identifiable data to a server. By default, it’s what is used as the key in the query stirng for a GET request, or in the body of a POST request. So you would think that it’s obvious we need it.

However, with the ever growing landscape of JavaScript frameworks, it’s become much easier to forego this attribute and simply bind some data object to some input. For example, in Vue:

<template>
  <form @submit="onSubmit">
    <input v-model="dataKey">
  </form>
</template>

<script>
  export default {
    data: () => ({
      dataKey: ''
    }),
    methods: {
      onSubmit() {
        console.log(this.dataKey)
      }
    }
  }
</script>

This would work fine in most cases, but as we mentioned above, it’s a good idea to keep progressive enhancement in mind. If this form was rendered on the server and JavaScript is disabled in the browser, the form wouldn’t work. Even if we include the form action, we would just send data without any sort of identifier.

More important than accounting for a non-JS world, is the fact that leaving out the name attribute can actually have negative user experience. We’ll look at that in more detail in a later article.

Moving on to labels.

I think this topic has been covered at length, but I’ll reiterate here. Every form input needs a label, ALWAYS.

Not a placeholder.

Not a title.

A label.

You have 3 options to work with:

  • Label wrapping the input: <label>Name: <input name="name"></label>
  • Label with for attribute: <label for="name">Name:</label><input name="name">
  • aria-label: Name: <input name="name" aria-label="Name">

Between the first two, to my knowledge, there isn’t a significant difference besides the need for a for attribute on the label, and an id on the input. For this reason alone, I prefer using option 1.

Update (2020/04/07): Reader Kate Kalcevich pointed out to me that, in fact there IS a difference between an implicit label (wrapping the input) and an explicit label (using a for attribute). The short version is that the explicit label is more accessible. More details in the next article.

Now let’s look at the third option, aria-label. aria-labels are very useful for adding contextual labels for elements without adding visual labels. This is great for accessible inputs (for screen-readers) with no visible labels (or labels that look like placeholders).

We will look more into labels in the Accessibility section, but for now I will say that my preferred way to add a label is the first option.

Include A Submit Button

For a while, I wondered whether a submit button was always required in a form. In many cases, it’s not obvious from the design that we need one. Take, for example, the following design.

Sexy search form, bro. Who needs submit buttons?

There’s a few problems with omitting a submit button:

  • It’s not obvious that the user has to hit the “Enter” key to submit (especially for screen-reader users).
  • It’s not valid HTML.
  • You might not get implicit submission (‘ooooh’)

Implicit submission is a browser feature that you may have experienced before. If you select a text input and press the “Enter” key, the form will be submitted. That behavior is explained in the HTML spec:

“If the user agent supports letting the user submit a form implicitly (for example, on some platforms hitting the “enter” key while a text field is focused implicitly submits the form), then doing so for a form whose default button has a defined activation behavior must cause the user agent to run synthetic click activation steps on that default button.”

HTML Specification

“If the form has no submit button, then the implicit submission mechanism must do nothing if the form has more than one field that blocks implicit submission, and must submit the form element from the form element itself otherwise.

For the purpose of the previous paragraph, an element is a field that blocks implicit submission of a form element if it is an input element whose form owner is that form element and whose type attribute is in one of the following states: Text, Search, URL, Telephone, E-mail, Password, Date and Time, Date, Month, Week, Time, Local Date and Time, Number”

HTML Specification

In other words (and it’s up to the browser implement), hitting the “Enter” key while focused on a text input will fire the click event on the form’s submit button. Many browsers still implement implicit submissions even if the submit button is missing, but I think it’s best to add a button anyway.

Here’s the two ways to write a submit button in HTML:

  • <input type="submit" value="Submit this form">
  • <button type="submit">Submit this form</button>

I prefer the <button type="submit">.

Note that if you omit the type attribute on a button within a <form> tag, it will assume it is a submit button. Clicking said button will result in the form submitting.

I try to make a habit of explicitly setting the type of any button in a form, because in some rare cases, I actually want a button of type button, in order to perform some action besides a submission (like a click-to-copy). So, it’s not always necessary to add the type attribute, but I consider it a good habit.

Oh yeah, and in regards to including a submit button for the example design above, we will look at visually hiding things like submit buttons in a later article.

Closing Thoughts

An example form following the practices we covered might look like so:

<form>
  <fieldset>
    <legend>Operating System</legend>
    <label>
      <input type="radio" name="os" value="windows">
      Windows
    </label>
    <label>
      <input type="radio" name="os" value="mac">
      Mac
    </label>
    <label>
      <input type="radio" name="os" value="linux">
      Linux
    </label>
  </fieldset>

  <fieldset>
    <legend>Favorite JS Frameworks</legend>
    <label>
      <input type="checkbox" name="ck-angular" value="angular">
      Angular
    </label>
    <label>
      <input type="checkbox" name="ck-react" value="react">
      React
    </label>
    <label>
      <input type="checkbox" name="ck-vue" value="vue">
      Vue
    </label>
    <label>
      <input type="checkbox" name="ck-svelte" value="svelte">
      Svelte
    </label>
  </fieldset>

  <label>
    Position
    <select name="position">
      <option value="" selected></option>
      <option value="fs">Full Stack</option>
      <option value="fe">Front End</option>
      <option value="be">Back End</option>
      <option value="de">Designer</option>
      <option value="pm">Project Manager</option>
      <option value="ceo">CEO</option>
    </select>
  </label>

  <label>
    Years of Experience
    <input name="exp" type="number">
  </label>

  <fieldset>
    <label>
      First Name
      <input name="fname">
    </label>
    <label>
      Last Name
      <input name="lname">
    </label>
  </fieldset>  

  <label>
    Email
    <input name="email" type="email">
  </label>

  <label>
    Comments
    <textarea name="comments"></textarea>
  </label>

  <button type="submit">Submit</button>
</form>

We went kind of deep on several topics that could otherwise seem superficial. After all, the main advice boils down to: Always use the <form> tag, use the right input types, wrap every input in a label, and include a submit button.

I hope it wasn’t boring, and hey, maybe you learned something new. My goal was to encourage you to think critically about the things you are building and use the tools that the platform already provides for us. Start with semantics and build up from there.

Please let me know what you thought, and stick around through the series because we have a lot to cover. Follow me on Twitter and sign up for the newsletter to get the latest updates.