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-300text, 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 to and focus the field when selected.
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 (for example,
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)