teaching machines

Infield Form Labels

April 27, 2020 by . Filed under howto, public, www.

I’ve done enough web development now and taught enough web development courses that I am starting to develop opinions. Or maybe I’ve had opinions since the start, but now I feel qualified enough to air them publicly. Today’s opinion is on labels for form elements. Let’s all agree that we need them. But where do we put them?

We could go with labels on the left and inputs on the right—with a grid to keep everything aligned. But the labels will have various lengths. Do we right-align them against the inputs, leaving a ragged axis on the left? Or do we left-align them, leaving internal space between the shortest labels and the inputs?

Or we could go with labels and inputs all in one vertical stack. The danger with this arrangement is ambigous visual parsing. Does a label associate with the input above it or below it?

I think neither approach is good. Both suffer from separateness. The label and input are distinct elements. Why not just put the label and the input in the same box? That’s easy enough to do in HTML.

<label class="infield">
  <span>Name</span>
  <input type="text">
</label>

<label class="infield">
  <span>Regrets</span>
  <input type="text">
</label>

<label class="infield">
  <span>Birth Order</span>
  <input type="text" class="number-input">
</label>

But these label-input pairs are very much separated when this HTML is rendered. We can hide this separateness with some CSS that visually blends the label, span, and input together. We strip out all of the input decorations and make the label look like one big box that we can type in. Really the label is just a vertical stack of its two children.

.infield {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  padding: 5px;
  border: 1px solid darkgray;
  border-radius: 3px;
  outline-width: 3px;
  outline-offset: -2px;
  background-color: #F6F6F6;
  box-shadow: inset 0 0 2px #999999;
}

.infield > span {
  color: darkgray;
  font-family: sans-serif;
  font-size: 12px;
  margin-bottom: 5px;
}

.infield > input {
  border: none;
  outline: none;
  background: none;
  font-size: 16px;
}

We end up with the form shown in Figure 1.

Figure 1. A form that places labels directly in the input fields.

What about our outline? Usually the browser shows us what form element we are focused on. Well, if we have :focus-within, we can apply the outline to the surrounding label.

.infield:focus-within {
  outline-color: blue;
  outline-style: auto;
}

.infield:focus-within > span {
  font-weight: bold;
  color: blue;
}

The form shown in Figure 2 is more like what we are used to.

Figure 2. A form that highlights the input currently focused.

Perhaps we could highlight the fields when something has been entered, or when something has been entered incorrectly. Let’s add some styles for these two states.

.infield:focus-within > span, .infield.non-empty > span {
  color: blue;
  font-weight: bold;
}

.infield.error {
  outline-color: red;
  outline-style: auto;
  background-color: #FFEEEE;
}

.infield.error > span {
  color: red;
  font-weight: bold;
}

Some JavaScript is needed to dynamically add or remove these classes when the user types in the boxes.

const infields = document.querySelectorAll('.infield');
for (let infield of infields) {
  const input = infield.children[1];

  input.addEventListener('input', () => {
    infield.classList.toggle('non-empty', input.value.length > 0); 
  });

  if (input.classList.contains('number-input')) {
    input.addEventListener('input', () => {
      infield.classList.toggle('error', input.value.length > 0 && !input.value.match(new RegExp('^\\d+$'))); 
    });
  }
}
Figure 3. A form with cues for non-empty and invalid inputs.

There we have it! A form that is clearly labeled and easy to align. All that’s left is to put a really big Submit button below it, as we see in Figure 4.

Figure 4. A form with a really big button.