Form

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

A real form, end to end

View “A real form, end to end” as
Your details

The receipt goes here.

Enter an email address, like ada@example.com.

Delivery address
Delivery speed
Anything else
<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.

The compact register

View “The compact register” as
<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.

Level 2 tokens declared by form
TokenDefault
--form-actions-gapvar(--space-3)
--form-actions-justifyflex-start
--form-actions-spacevar(--space-2)
--form-gapvar(--space-6)
--form-row-gapvar(--space-4)
--form-row-min12rem
--form-section-gapvar(--space-5)
--form-section-title-colorvar(--color-on-surface)
--form-section-title-font-familyvar(--font-primary)
--form-section-title-font-sizevar(--font-size-base)
--form-section-title-font-weightvar(--font-weight-semibold)
--form-section-title-gapvar(--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

  1. 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.
  2. The submit button is type="submit". A <button> with no type inside a form is already a submit button, but a type="button" that calls script instead removes Enter-to-submit from every field in the form.
  3. 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-reverse on the actions leaves the tab order pointing the other way.

src/css/components/form.css · npx mostlycss add form