Form

elements man style/components/form Read as markdown

Form 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.