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()
.