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>
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.
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: block
ing 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()
.