Design HubStudio

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.

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 alignment
    Principle 2
    Flex with flex: 1 on all non-label children stretches fixed-width controls — switches and radios end up spanning the full row, and the thumb travel breaks. Use CSS Grid grid-template-columns: 140px 1fr instead; inputs fill the 1fr column via their own width: 100%, switches stay at their native size. We hit this exact bug when wiring the composition below.
  • Don't use min-width on labels
    Principle 2
    min-width lets longer labels push controls rightward, creating jagged column alignment across rows. Use fixed width (or the grid equivalent) so the control column stays aligned regardless of label content.
  • Don't use bare <input type="checkbox"> for single toggles
    Principle 4
    The 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 below --space-3
    Principle 5
    --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 message
    UX
    A red border with no explanation is hostile — the user knows something's wrong but not what. Always pair aria-invalid="true" with a .form-error message below the input, naming the rule that failed.
  • Don't show error text above the input
    UX
    Errors 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.

Device configuration
42 plates/hr
  • grid not flex 140px label column + 1fr control column — fixed-width controls (switch, radio) sit at their natural size, not stretched
  • native widths Switch is 28px; slider has a paired value; textarea grows vertically — each control behaves like itself
  • one primary Save = primary, Cancel = ghost. Footer reads left-to-right: exit, then commit
  • space-3 between rows Generous gap; the form doesn't feel cramped at any width