Form controls
Form controls
One control vocabulary, read by every editable surface. Before it, the app had five private ones — a text/select bundle at one size, the parameter table's two hand-written controls, the request bar's box and the tree filter — with four different focus answers and no shared height. A reader crossing the playground met a 44px address field, a 34px grid cell and a 30px filter, each answering a click differently, and learned the interface three times.
InputComponent and SelectComponent are that vocabulary now, over one
geometry scale and one state contract in
ControlStyles.
Everything below is rendered from those components and the same tokens the rest
of the pipeline is generated from.
The scale, and where each size goes
A size is a box, not a typeface: it fixes height, inline padding and radius, not how hard the value is to read.
| Size | Box | Text | Where |
|---|---|---|---|
| sm | 28px | 14px | parameter-table cells, the tree filter |
| md | 36px | 14px | every form on a page — the default |
| lg | 44px | 16px | the request address; touch targets |
sm and md share 14px type on purpose. The sm size exists for the parameter
table, and the parameter table is where a reader reads values — coffee shops
berlin, 227 country codes. A 12px mono value in a grid is a legibility defect;
"small" here means the box got tighter, not that the content got harder to read.
lg steps to 16px because it is the one control on screen the whole time and the
one a thumb aims at.
The seven states, once
Rest, hover, focus, filled, read-only, disabled and error are defined once and
are identical on input and select at the same size — an input and a select
of the same size are the same box, which is the whole claim. Hover deepens the
border to ink-faint, never the accent: a hover that lit the accent read as a
promise of action rather than as a hover.
The hover and focus rows above are drawn by preview-only classes the template declares — a static page has no pointer. They exist nowhere in the application's stylesheet.
Focus is the family's one exception
The site's global recipe is :focus-visible { outline: 2px solid accent-line },
and every button, link and icon control keeps it. Text and select controls take
a different one — an accent-line border plus a 3px focus-ring, on :focus,
with the outline suppressed — because :focus-visible does not match a mouse
click into an input on every engine, and a caret blinking inside a 1px box is
not a state. This is the family's only departure from the global recipe, it is
declared once, and it adds no second ring to buttons or links.
Suppressed means suppressed in the stylesheet, beside the recipe it excepts —
not by the control's own outline-none. The global rule sits outside every
cascade layer so that a utility cannot silently remove a focus state, which means
a utility cannot remove this one either: the family's border and ring shipped
under the global outline until the stand-down was written, and a focused field
wore two borders for one state. The exception names the controls that draw their
own focus and leaves the outline to everything that does not — a checkbox, a
radio and the browser's own widgets have no other focus state to fall back on.
Error is a border and a sentence
An invalid control carries a 1px border and ring in the status-error role and
aria-invalid; the surface that owns the control — the parameter grid's cell —
states the message beneath it in that same role. The
old 2px-border trick is gone — a weight change is not a state a reader can name —
and a validation message is a sentence, not a Badge: a Badge labels a
meaning, and the control never recolours its own value.
Quiet is a variant, not an absence
The parameter table's cell is already ruled on three sides, so a bordered field
inside it states the same edge twice. quiet withholds the resting border and
fill and gets them back on hover and focus — the family's own border, the
family's ring, the family's disabled treatment. A quiet control is a skin,
never a different control, which is the sentence that keeps the next person
from re-inventing a borderless input somewhere else.
Deliberately not done
- No custom listbox. The native select's viewport handling, type-ahead and
phone behaviour are worth more than a matching chevron animation; the only
thing
appearance: nonecosts is the arrow, which the family draws. - No size below 28px, and no fourth size — the densest surface there is is comfortable at 28px.
- No 12px control type at sm — a 12px mono value in the grid is a legibility defect, not a density setting.
- No red error text inside a control, and no checkbox/radio redesign — the send box and the segmented control keep their markup and only join the scale.