Forms
On this page
Layout
Single-column is the default. Multi-column only when:
- Fields are logically paired (min/max, start/end, from/to), or
- The form has 15+ fields and vertical scrolling becomes operator-hostile.
Column width
single-column forms cap at 40ch (~320px) - readable one-line width. Two-column layouts use two 30ch columns with a 24px gap between.
Field density
standard = 16px vertical gap between fields. Compact (operator-dense internal admin) = 8px. Never smaller; at that tier, structural grouping is cleaner than further tightening.
Labels
Labels are always top-aligned and always present. Placeholder text is not a label - it disappears when the user types and fails accessibility.
- Position: above the field, 4px gap
- Type: Inter 500 13px,
fog-100 - Required fields: append a
steel-300asterisk*after the label text. Do not color the asterisk red - it’s not an error state, it’s a requirement. Don’t append “(required)” either - the asterisk is the convention. - Optional fields: no indicator. The default assumption is optional; required is the marked case.
Side-aligned labels (label to the left of the field) are permitted only for dense internal admin forms where vertical space is at a premium. Never on operator-facing surfaces.
Help text
- Position: below the field, 4px gap
- Type: Inter 400 12px,
steel-300 - Length: one line preferred, two lines maximum
- Purpose: disambiguation, format hint (
UTC timestamp, YYYY-MM-DDTHH:MM:SSZ), or field-specific guidance. Not marketing copy, not elaboration on why the field exists.
Validation
Timing
- Inline validation fires on blur, not on every keystroke. Fighting the user as they type is hostile.
- Exception: password-strength meters and character counters, which update on input because that IS the feedback.
- Submission validation runs on submit and focuses the first invalid field.
Display
- Inline error - below the field, replacing the help text.
hostile-400text, Inter 400 12px.triangle-exclamationicon 12px inline left of the error text. - Field state - invalid field has a 2px
hostile-400border (replacing the defaultnavy-600) plus--fdt-glow-xsin hostile tint. - Summary at top of form - only when 3+ fields are invalid. Uses
wa-callout variant="danger". Lists field labels as anchor links that scroll/focus to the field on click.
Error copy
State what’s wrong and how to fix, in that order. Never just “required.” The tone is doctrinal and declarative - never apologetic, never conversational.
| Do | Don’t |
|---|---|
Enter a mission code - 3 to 12 uppercase characters |
Oops! Please enter a valid mission code |
Date must be in the future |
That date can't be right |
Password must contain at least one number |
Weak password |
Invalid MGRS grid reference |
That doesn't look like an MGRS grid |
Value exceeds maximum of 1,000 |
Value is too high |
Never use “please.” This is a console, not a service request.
Field sizing
Field widths match the data they accept. A phone number field does not need to be full-column wide.
| Field type | Width |
|---|---|
| Short identifiers (codes, IDs) | 8–12 characters |
| Small numbers (<4 digits) | 6 characters |
| Coordinates (pair) | Two 12-character fields side-by-side |
| Dates / times | date picker + 10–19 character text fallback |
| Email address | full column |
| Name | full column |
| URL | full column |
| Free-text comment / description | full column, multi-line |
Making every field full-column wastes space and implies the data is more open-ended than it is.
Sections and grouping
For forms with 8+ fields, group related fields into sections:
- Section heading: H3, Inter 600 14px UC tracked 0.08em,
fog-100, with anavy-6001px underline - Section gap: 32px above, 16px below the heading
- No visible fieldset border - the heading and spacing do the work
For complex multi-section forms (mission authoring, program creation, user setup), consider a multi-step wizard instead of one long form.
Multi-step forms (wizards)
When a form has 5+ logical sections or takes more than ~2 minutes to complete:
- Step indicator at the top - horizontal steps with current / complete / upcoming states. JetBrains Mono 10px step number + Inter 500 11px label.
- Per-step validation on Next - can’t advance past an invalid step.
- Back / Next / Save as draft buttons at the bottom, right-aligned. Save as draft is optional but encouraged for forms users will return to.
- Never auto-advance - the user hits Next explicitly, always. An auto-advancing wizard feels like it’s racing the operator.
- Progress persists - if the user refreshes or navigates away, the wizard resumes at the step they left, with the partially-filled data intact.
Submit actions
- Primary submit: right-aligned at the bottom of the form,
wa-button variant="brand", label matches the action -Create mission,Save changes,Send invite. NeverSubmit. - Secondary (cancel): left of primary,
wa-button variant="neutral" appearance="plain", labelCancelorDiscard. - Destructive secondary (e.g.,
Delete user): far left,wa-button variant="danger" appearance="plain", separated from the other actions by a flex gap. - Never full-width submit buttons outside of QRF mobile. Full-width submit is a consumer-app pattern.
Dirty-form warnings
If the user navigates away from a form with unsaved changes:
- Intercept the navigation attempt
- Show a
wa-dialogwith: “You have unsaved changes to {form name}. Discard them?” - Primary action:
Keep editing(stays on form,brandvariant) - Secondary action:
Discard changes(leaves form,dangervariant) - Do not offer a
Saveoption in the dialog - saving is a different action with its own button on the form. The dialog’s job is to protect against accidental loss, not to become a mini-form.
Autofocus
- Autofocus the first empty or invalid field on mount
- Do not autofocus if the form is embedded in a page with controls above it (classification strip, primary nav) - autofocus on mount can trap screen-reader users
- Do not autofocus on mobile - triggers the keyboard immediately, often obscuring form context
Accessibility
- Every field has a
<label>associated viafor/id, not just a visible text element - Required fields:
requiredattribute + asterisk in label +aria-required="true" - Error state:
aria-invalid="true"+aria-describedbypointing to the error text element - Groups:
<fieldset>+<legend>for radio groups and related checkbox sets - Keyboard: Tab traversal follows visual order; Shift+Tab reverses; Enter submits the form (unless focus is inside a textarea)
- Focus visible: the two-layer focus ring (2px
electric-500border +--fdt-glow-sm)