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
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 one-column grid with a gap. Lay it out however you like and the gap goes with the layout, because it belongs to the form rather than to its children:
Row form<form class="row"> <input placeholder="What needs doing?"> <button class="is-primary">Add</button> </form>
display: flex on the form does the same thing, and so does any grid of your
own. There is nothing to switch off first: no rule puts a vertical step on the
children, so nothing is left behind to push a button below the input beside it.
Set your own gap to override the inherited one.
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.
Checkbox and radio<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 |
Modifiers<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:
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 |
Field with error<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 error<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:
Grid form<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.