5. Forms
The hardest section to get right. Alignment, control semantics, validation, and rhythm each have their own taste decisions — and they compound.
Gallery
Form primitives in isolation. States via :hover, :focus, :disabled, and aria-invalid.
.form-input · default.form-input · aria-invalid="true".form-input · :disabled.form-select.form-textarea.switch · .switch-sm.checkbox · checked / unchecked / mixed.radio · checked / unchecked.slider
When to use each
Three decisions that compound in every form: how to lay out labels, which binary control to use, and how to give feedback.
Label layout — above vs. beside
Above the input
Stacks vertically; scans top-to-bottom. Use for primary content forms (signup, contact, single-purpose flows) and narrow viewports. Each field stands on its own.
Beside the input (grid)
Labels in a fixed left column, controls aligned right. Use for settings, admin, detail panels — anywhere visual alignment across rows matters. Default for Titan-style configuration UI.
Switch vs. checkbox
Send daily summary email
Switch — binary setting
Single on/off toggle. State changes immediately when flipped (real-time setting, no submit step). Use in detail panels, preferences, feature toggles.
Checkbox — multi-select
One of many. Use for multi-select from a list, staged opt-ins (terms acceptance), or boolean fields applied on submit. Implies "pick zero or more."
Helper vs. error text
We'll never share your email.
Helper — persistent guidance
Hints, format examples, scope clarifications. Always visible. Use to head off mistakes before the user makes them.
Email format is invalid.
Error — validation feedback
Appears after blur or submit on invalid input. Replaces or sits below the helper. Always pair with
aria-invalid="true" on the input — never a red border without explanation. Drift to avoid
Six named anti-patterns. The first three came directly from either the audit or shipping bugs we hit building this kitchen sink.
- Don't use flex for form row alignmentPrinciple 2Flex with
flex: 1on all non-label children stretches fixed-width controls — switches and radios end up spanning the full row, and the thumb travel breaks. Use CSS Gridgrid-template-columns: 140px 1frinstead; inputs fill the 1fr column via their ownwidth: 100%, switches stay at their native size. We hit this exact bug when wiring the composition below. - Don't usePrinciple 2
min-widthon labelsmin-widthlets longer labels push controls rightward, creating jagged column alignment across rows. Use fixedwidth(or the grid equivalent) so the control column stays aligned regardless of label content. - Don't use barePrinciple 4
<input type="checkbox">for single togglesThe audit found this in 25+ files. Single boolean toggle =.switch. Multi-select from a list =.checkbox. The two controls have different ARIA semantics and different user expectations; the wrong one feels off even when the user can't articulate why. - Don't cram label-to-control gaps belowPrinciple 5
--space-3--space-1(4px) and--space-2(8px) read as cramped — fields blur together and the form feels frantic.--space-3(12px) minimum between label and control.--space-4(16px) between adjacent rows. - Don't pair invalid state without an error messageUXA red border with no explanation is hostile — the user knows something's wrong but not what. Always pair
aria-invalid="true"with a.form-errormessage below the input, naming the rule that failed. - Don't show error text above the inputUXErrors belong below the input, where the eye lands after typing. Above-input errors get missed; below-input errors get read.
Composition — device configuration
A real settings form using everything above. Labels beside the input (grid), controls aligned across rows, switch at native width, slider with paired value, footer with one ghost + one primary.