Skip to content
Fleetline

Form field

Usage

Every Fleetline form control sits inside a form field: a visible label, the control, optional helper text, and an error message when validation fails. The field wires these together with for/id and aria-describedby once, so every control behaves the same.

Anatomy

  1. Field — .field, a grid with a 6px gap
  2. Label — label.label[for], with optional .label__required or .label__optional
  3. Control — any form control (.input, select, textarea…)
  4. Helper text — .field__hint, referenced by aria-describedby
  5. Error — .field__error: icon + visually hidden "Error:" + message, referenced by aria-describedby
  6. Counter (optional) — .field__counter, updated by js/form-field.js

Examples

Label

Fields marked * are required.

<label class="label" for="vin">Vehicle identification number (VIN) <span class="label__required" aria-hidden="true">*</span></label>
<input class="input" id="vin" name="vin" type="text" required>   <!-- required attribute, not only the asterisk -->

<label class="label" for="nickname">Fleet nickname <span class="label__optional">(optional)</span></label>

Form field

In kilometres, as shown on the dashboard.

In kilometres, as shown on the dashboard.

Error: Enter a reading higher than the last one (84,210 km).

Mechanics see this on their task list. Keep it short.

Up to 60 characters

Where the vehicle parks overnight.

<div class="field">
  <label class="label" for="odometer">Odometer reading</label>
  <input class="input" id="odometer" name="odometer" inputmode="numeric"
         aria-invalid="true" aria-describedby="odometer-hint odometer-error">
  <p class="field__hint" id="odometer-hint">In kilometres, as shown on the dashboard.</p>
  <p class="field__error" id="odometer-error">
    <svg class="field__error-icon" aria-hidden="true" focusable="false"><use href="#icon-alert-circle"></use></svg>
    <span><span class="sr-only">Error: </span>Enter a reading higher than the last one (84,210 km).</span>
  </p>
</div>

<!-- Counter: js/form-field.js → init(root) -->
<input class="input" id="title" data-counter="title-count" aria-describedby="title-count">
<p class="field__counter" id="title-count" data-max="60">Up to 60 characters</p>

<!-- Horizontal from 48rem -->
<div class="field field--horizontal">…</div>

Fieldset

Depot address
<fieldset class="fieldset">
  <legend class="fieldset__legend">Depot address</legend>
  <div class="fieldset__body">
    <div class="field">…Street address…</div>
    <div class="field">…Town or city…</div>
    <div class="field" style="--field-max: 12rem">…Postcode…</div>
  </div>
</fieldset>

API

HookValuesPurpose
.field, .field--horizontalblock / modifierField wrapper; label beside control from 48rem
--field-maxcustom propertyMax width (default 32rem); size fields to the expected answer
.label, .label__required, .label__optionalblock / partsLabel and its markers
.field__hint, .field__error, .field__error-icon, .field__counterpartsHelper, error, counter (reference all by aria-describedby)
required, aria-invalid="true"attribute on the controlRequired and invalid states
data-counter="<counter id>", data-maxattributeEnables the live counter (js/form-field.js, init(root))
.fieldset, .fieldset__legend, .fieldset__bodyblock / partsGrouped controls

Do and don't

In kilometres.

Do keep a visible label and put format help in the hint.
Don't use the placeholder as the label; it disappears as soon as someone types.

Accessibility

Keyboard interaction
KeyBehavior
Tab / Shift+TabMoves between controls in reading order; labels, hints and legends aren't focusable

Tokens

Custom propertyPurpose
--font-size-sm | -md, --font-weight-semiboldLabel and legend type
--color-text-default | -mutedLabel, hint and counter text
--color-feedback-danger-fg | -iconError message, required marker and error icon
--space-1-5 | -4Gap inside a field; gap between fields
--icon-size-sm, --icon-strokeError icon