Styling forms

There’s a lot of resources floating around about styling form controls – I like Adrian Roselli’s ‘underengineered’ posts – but I wanted to write down how to actually lay out the form.

Most form controls are inline elements, so the “default” is to create single-line forms. Sometimes this is fine, such as the checkbox above. Other times it is less fine:

<label for=name>Name</label><input type=text id=name>
<label for=email>Email</label><input type=text id=email>
<label for=password>Password</label><input type=password id=password>
<button>Sign up</button>

You can slap each group of fields in a block-level container, but now you have an ugly ragged form, and the controls butt up against the label.

<p><label for=name>User name</label><input type=text id=name></p>
<p><label for=email>Email</label><input type=text id=email></p>
<p><label for=password>Password</label><input type=password id=password></p>
<p><button>Sign up</button></p>

Labels above controls

You can solve both problems by simply forcing linebreaks with label { display: block; }. Of course this changes the layout of the form.

label {
  display: block;
}

<div>
<p><label for=name>User name</label><input type=text id=name></p>
<p><label for=email>Email</label><input type=text id=email></p>
<p><label for=password>Password</label><input type=password id=password></p>
<p><button>Sign up</button></p>
</div>

The block-level element stretches to the width of the container, which gives it a strange clickbox. There are ways around this, like assigning display: block to a wrapper element and letting that grow instead of to the label itself. Maybe a single-column grid or flexbox. TODO.

Labels beside controls

You can do it with a table, but… let’s not.

You can also do it with a grid. Here, grid-template-columns: max-content max-content creates two columns with the width of the widest element inside each column, much like a <table> would. Unlike a table, you get much better accessibilty characteristics out of the box, tidier HTML, and can easily make it single-column (or even display: blocking everything) on a smaller screen.

I’m using a spacer div to kick the <button> into the second column. For a non-toy example it would be better to use an explicit grid-column.

Using justify-self on the <button> prevents the grid from stretching it to the width of its column. text-align is used on the <label>s, instead of justify-self, because this time I want the grid to stretch their clickbox.

form {
  display: grid;
  grid-template-columns: max-content max-content;
  gap: 5px 15px;
}

button {
  justify-self: start;
}

label {
  text-align: right;
}

<form>
  <label for=name>User name</label>
  <input type=text id=name>
  <label for=email>Email</label>
  <input type=text id=email>
  <label for=password>Password</label>
  <input type=password id=password>
  <div></div>
  <button>Sign up</button>
</form>

How about this: Only set row-gap in the grid, and instead create the gap with the padding-right of the <label>s. This closes up the clickbox.

As always when using CSS grid, make sure to test for overflow. Grid is very rigid. Consider minmax(), fit-content().