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.
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.
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.
Rich dropdown — when native select isn't enough
Native <select> can't show a color swatch next to an option or a description line below it. When you need either, compose a .form-dropdown-trigger (looks identical to .form-select) with a .popover hosting .form-dropdown-option rows. DH ships the CSS; you wire the open/close, keyboard nav, and ARIA.