Form
elements man style/components/form Read as markdownForm controls style themselves. A bare <input>, <textarea>, or <select> is
a finished 40px control with hover, focus, disabled, and readonly states.
Checkboxes and radios are custom-drawn. The .field wrapper adds label and
help/error structure, and form.is-grid gives a two-column layout. This is
form.css in @layer components.
Because the bare styling is applied with :where() at zero specificity, any
class you add wins. Modifiers go on the control element, not the wrapper.
Bare controls
<input type="email" placeholder="you@company.com">
<textarea placeholder="Notes…"></textarea>
<select>
<option>One</option>
<option>Two</option>
</select>
Text inputs (all the common types), textarea, and select share the 40px
chrome, a --rule border that strengthens on hover, and an accent border plus a
1px ring on focus. <textarea> is taller (min 6rem) and resizes vertically.
<select> draws a custom caret (the --select-caret glyph). Disabled and
readonly controls get a soft fill.
A form laid out as a row
A bare form is a vertical stack, and base.css spaces its children with
margin-block-start because a bare form has no wrapper to hang a gap on. A
form you lay out horizontally is not that, so say so with row:
<form class="row">
<input placeholder="What needs doing?">
<button class="is-primary">Add</button>
</form>
row, stack and cluster all clear that flow margin and bring their own
gap. Reaching for a bare display: flex on the form instead leaves the margin
in place, and it shows up as every control after the first sitting a step lower
than the one beside it. CSS cannot ask whether an element is a flex container,
so the class is what tells us.
Checkbox and radio
<input type="checkbox"> and <input type="radio"> are custom-drawn: an 18px
box (square for checkbox with --radius-xs, round for radio), a strong border
that turns accent on hover, and an accent fill when checked. The checkbox draws
its check from the --checkbox-check glyph; the radio draws its dot with a
gradient in --accent-ink.
<label><input type="checkbox" checked> Email me updates</label>
<label><input type="radio" name="plan"> Monthly</label>
Sizes and states (on the control)
| Class | Effect |
|---|---|
is-sm |
32px control |
is-lg |
48px control |
is-error |
Danger border (and danger focus ring) |
is-ok |
Success border |
<input class="is-lg is-error" value="not-an-email">
The field wrapper
.field wraps a label, a control, and help or error text into a labeled field:
<div class="field">
<label>Email</label>
<input type="email">
<p class="hint">We'll never share it.</p>
</div>
Parts:
| Selector | Role |
|---|---|
.field > label |
The field label |
.field > .head |
A label row with a right-aligned slot (baseline-aligned, space-between) |
.field > .head > label |
The label inside a head row |
.field > .head > .helper / .field > .head > a |
The right-aligned helper or link (e.g. "Forgot?") |
.hint |
Small --ink-soft helper text. Inside a field it sits below the control |
.error |
Danger-colored error text. Inside a field it sits below the control |
.field.is-full |
Spans both columns in a grid form |
<div class="field">
<div class="head">
<label>Password</label>
<a href="/reset">Forgot?</a>
</div>
<input type="password" class="is-error">
<p class="error">Password is required.</p>
</div>
.hint and .error work anywhere, not only inside a .field. An error that
belongs to the whole form rather than to one input goes in the form:
<form>
<p class="error">That email and password do not match.</p>
<div class="field">...</div>
</form>
The only thing .field adds is the margin that spaces the text under a
control. Put is-error on the input as well when the error points at one
field, so the control gets the danger border and focus ring.
Grid form
<form class="is-grid"> lays fields out in two columns with consistent gaps,
and collapses to a single column at 40rem. Use .field.is-full for a field that
should span the full width:
<form class="is-grid">
<div class="field"><label>First name</label><input></div>
<div class="field"><label>Last name</label><input></div>
<div class="field is-full"><label>Email</label><input type="email"></div>
</form>
Fieldset
Bare <fieldset> gets a --rule border and padding; <legend> renders as a
small medium-weight --ink-soft label. No classes needed.