Form layout
Usage
Use this layout for every page-level form in Fleetline: logging a service, creating a work order, editing a vehicle. Dialog forms reuse the same fields but put actions in the modal footer.
- One column, label above the control. It is the fastest to scan and complete, and it survives gloves, tablets and 400% zoom.
- Two columns only for short related fields (labour hours + parts cost, town + postcode), and only once the form is at least 28rem wide.
- Sections with a heading and one sentence of description once a form has more than about six fields. Use
fieldset+legendwhen the group is one question (which checks were done?), asectionwith a heading when it is just a topic. - Actions: primary first (inline-start), then Cancel as a secondary button. Verb + noun labels: "Save service log", not a generic word. On long forms the action bar sticks to the bottom of the screen.
- Errors: validate on submit. Show an error summary at the top, move focus to it, and repeat each message next to its field. Prefix the page
<title>with "Error: ". - Mark the minority: if most fields are required, mark the optional ones "(optional)".
Anatomy
- Error summary (invalid state) —
.alert.alert--danger.form-layout__summarywith links to each field - Section —
section.form-layout__section:.form-layout__intro(heading + description) and.form-layout__fields - Fields —
.field,fieldset.fieldset,fieldset.date-field,fieldset.checkbox-group - Row (optional) —
.form-layout__rowfor two short related fields - Divider —
hr.dividerbetween sections - Action bar —
.form-layout__actions, optionally--sticky
Examples
Form layout
variant:single-column — quick log from a tablet
variant:two-column — short related fields only
Labour and parts sit side by side from 28rem of form width and stack below it. Longer fields always keep the full width.
variant:sections variant:sticky-actions — full "Log service" form, scroll inside the frame
Three sections with headings and one-line descriptions, separated by dividers. Scroll the frame: the action bar stays at the bottom edge, primary first.
state:invalid — after submit, focus moves to the summary
The summary title counts the problems; each link moves focus to its field and repeats the inline message word for word. Entered values are kept.
<form class="form-layout" action="/service-logs" method="post" novalidate aria-labelledby="log-t">
<!-- after a failed submit: render the summary and focus it -->
<div class="alert alert--danger error-summary form-layout__summary" tabindex="-1" aria-labelledby="sum-t">
<svg class="alert__icon" aria-hidden="true">…</svg>
<div class="alert__content">
<h2 class="alert__title" id="sum-t">There are 2 problems with this service log</h2>
<ul class="list alert__description"><li><a class="link" href="#odometer">Enter a reading higher than the last one</a></li>…</ul>
</div>
</div>
<section class="form-layout__section" aria-labelledby="s-vehicle">
<div class="form-layout__intro"><h2 class="text-h5" id="s-vehicle">Vehicle</h2><p class="text-small text-muted">…</p></div>
<div class="form-layout__fields">
<div class="field">…</div>
<div class="form-layout__row"><div class="field">…</div><div class="field">…</div></div>
</div>
</section>
<hr class="divider">
…
<div class="form-layout__actions form-layout__actions--sticky">
<button type="submit" class="btn btn--primary">Save service log</button>
<a class="btn btn--secondary" href="/vehicles/kx-219">Cancel</a>
</div>
</form>
API
| Hook | Values | Purpose |
|---|---|---|
.form-layout | block on <form> | Single column, max --size-container-sm; size container for rows |
.form-layout__section | __intro | __fields | parts | Topic group, its heading + description, the field stack |
.form-layout__row | part | Two short related fields side by side from 28rem |
.form-layout__actions + --sticky | part + modifier | Action bar; sticky to the bottom of the scrollport |
.form-layout__summary | part on .alert--danger | Error summary; tabindex="-1" so it can take focus |
aria-invalid="true" + .field__error | attribute + part | Inline error, referenced by aria-describedby |
Do and don't
Accessibility
| Moment | Behavior |
|---|---|
| Submit with errors | Focus moves to the error summary (tabindex="-1"); its heading is read first, then the list of links |
| Enter on a summary link | Moves focus to the field (or the first input of a date field); the inline error is read through aria-describedby |
| Tab | Follows the visual order: sections top to bottom, row fields left to right, then the action bar |
- Every control has a visible
<label>; groups usefieldset+legend. - Errors are never color alone: icon, thicker border and a message that starts with visually hidden "Error:".
- The sticky action bar never covers the focused field: focused elements scroll into view above it (WCAG 2.4.11). Keep it to one row of buttons.
- Without JavaScript the server renders the summary and inline errors; with it, the same markup is inserted and focused.
Tokens
| Custom property | Purpose |
|---|---|
--size-container-sm | Maximum form width |
--space-8, --space-5, --space-4, --space-3, --space-1 | Between sections, fields, row columns, actions, heading and description |
--color-bg-surface, --color-border-default, --z-sticky | Sticky action bar surface, top rule and stacking |