Form
Form — the arrangement of a whole form: the rhythm between fields, the grouping of related fields under a legend, a pair of fields that belong side by side, and the actions at the end.
Examples
<form class="form" action="#" method="post">
<fieldset class="form-section">
<legend class="form-section-title">Your details</legend>
<div class="form-row">
<div class="field">
<label class="field-label" for="ck-first">First name</label>
<input class="input" id="ck-first" name="given-name" type="text"
autocomplete="given-name" required />
</div>
<div class="field">
<label class="field-label" for="ck-last">Last name</label>
<input class="input" id="ck-last" name="family-name" type="text"
autocomplete="family-name" required />
</div>
</div>
<div class="field">
<label class="field-label" for="ck-email">Email</label>
<input class="input" id="ck-email" name="email" type="email"
autocomplete="email" required
aria-describedby="ck-email-help ck-email-error" />
<p class="field-help" id="ck-email-help">The receipt goes here.</p>
<p class="field-error" id="ck-email-error">Enter an email address, like ada@example.com.</p>
</div>
<div class="field">
<label class="field-label" for="ck-phone">Phone</label>
<input class="input" id="ck-phone" name="tel" type="tel" autocomplete="tel" />
</div>
</fieldset>
<fieldset class="form-section">
<legend class="form-section-title">Delivery address</legend>
<div class="field">
<label class="field-label" for="ck-street">Street and number</label>
<input class="input" id="ck-street" name="street-address" type="text"
autocomplete="street-address" required />
</div>
<div class="field">
<label class="field-label" for="ck-extra">Flat, floor, company</label>
<input class="input" id="ck-extra" name="address-line2" type="text"
autocomplete="address-line2" />
</div>
<div class="form-row">
<div class="field">
<label class="field-label" for="ck-postcode">Postcode</label>
<input class="input" id="ck-postcode" name="postal-code" type="text"
autocomplete="postal-code" required />
</div>
<div class="field">
<label class="field-label" for="ck-city">City</label>
<input class="input" id="ck-city" name="address-level2" type="text"
autocomplete="address-level2" required />
</div>
<div class="field">
<label class="field-label" for="ck-country">Country</label>
<select class="select" id="ck-country" name="country" autocomplete="country">
<option>Italy</option>
<option>France</option>
<option>Germany</option>
<option>Spain</option>
</select>
</div>
</div>
</fieldset>
<fieldset class="form-section form-section-tight">
<legend class="form-section-title">Delivery speed</legend>
<div class="field field-inline">
<input class="radio" id="ck-standard" name="speed" type="radio" value="standard" checked />
<label class="field-label" for="ck-standard">Standard — 3 to 5 working days, free</label>
</div>
<div class="field field-inline">
<input class="radio" id="ck-express" name="speed" type="radio" value="express" />
<label class="field-label" for="ck-express">Express — next working day, €9</label>
</div>
<div class="field field-inline">
<input class="radio" id="ck-pickup" name="speed" type="radio" value="pickup" />
<label class="field-label" for="ck-pickup">Collect from a locker — free</label>
</div>
</fieldset>
<fieldset class="form-section">
<legend class="form-section-title">Anything else</legend>
<div class="field">
<label class="field-label" for="ck-note">Delivery note</label>
<textarea class="input" id="ck-note" name="note" rows="3"
placeholder="Leave it with the neighbour at number 4."></textarea>
</div>
<div class="field field-inline">
<input class="check" id="ck-invoice" name="invoice" type="checkbox" />
<label class="field-label" for="ck-invoice">I need an invoice</label>
</div>
<div class="field field-inline">
<input class="check" id="ck-news" name="news" type="checkbox" checked />
<label class="field-label" for="ck-news">Email me when something I have bought goes on sale</label>
</div>
</fieldset>
<div class="form-actions">
<button class="btn btn-primary" type="submit">Place the order</button>
<button class="btn btn-quiet" type="button">Back to the basket</button>
</div>
</form>A whole checkout, because a three-field example proves nothing about the one thing this component owns: the rhythm.
Four sections, three row-pairs, a radio group, two checkboxes and a textarea, and the vertical spacing between every one of them comes from two tokens. That is the argument — a form with inconsistent gaps is not a taste failure, it is a form whose grouping lies, since proximity is how a sighted reader perceives which fields belong together. .form-row folds to one column on its own when its children can no longer have room, with no media query and nothing for you to remember; narrow the window and the postcode/city/country row proves it. Every field carries a real autocomplete token, which is the part no stylesheet can supply and the part that decides whether the browser can fill any of this in.
<form class="form" style="--space-scale: 0.85" action="#" method="get">
<div class="form-row">
<div class="field">
<label class="field-label" for="fl-q">Search</label>
<input class="input input-sm" id="fl-q" name="q" type="search" placeholder="Order id, email, name" />
</div>
<div class="field">
<label class="field-label" for="fl-status">Status</label>
<select class="select select-sm" id="fl-status" name="status">
<option>Any</option>
<option>Paid</option>
<option>Shipped</option>
<option>Refunded</option>
</select>
</div>
<div class="field">
<label class="field-label" for="fl-from">From</label>
<input class="input input-sm" id="fl-from" name="from" type="date" />
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary btn-sm" type="submit">Apply</button>
<button class="btn btn-quiet btn-sm" type="reset">Clear</button>
</div>
</form>There is no .form-compact. Density is --space-scale, the multiplier the spacing scale already provides, scoped to the element that wants it — inventing a class beside it would be a second way of saying the same thing, and the two would drift.
The control sizes are separate on purpose: --space-scale tightens the space between things and leaves every target at its accessible floor, which is what you want on a filter bar and not something a density class should be allowed to override.
Tokens 12
Level 2, declared on .form itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --form-actions-gap | var(--space-3) |
| --form-actions-justify | flex-start |
| --form-actions-space | var(--space-2) |
| --form-gap | var(--space-6) |
| --form-row-gap | var(--space-4) |
| --form-row-min | 12rem |
| --form-section-gap | var(--space-5) |
| --form-section-title-color | var(--color-on-surface) |
| --form-section-title-font-family | var(--font-primary) |
| --form-section-title-font-size | var(--font-size-base) |
| --form-section-title-font-weight | var(--font-weight-semibold) |
| --form-section-title-gap | var(--space-4) |
Variants and states
Variants
.form-actions-end.form-section-tight
Inside it
.form-row.form-section.form-actions.form-section-title
State it reads
None.
Before you ship it
What you have to do 3 requirements
- Keep the
<legend>the first child of the<fieldset>. It is what names the group; a<p>styled to look like a heading names nothing. - The submit button is
type="submit". A<button>with notypeinside a form is already a submit button, but atype="button"that calls script instead removes Enter-to-submit from every field in the form. - Order the actions so the primary one comes first in the DOM where the visual order allows it, and never reorder them with CSS:
flex-direction: row-reverseon the actions leaves the tab order pointing the other way.
src/css/components/form.css · npx mostlycss add form