Forms
The form primitives. Field is the orchestrator — it generates the ids and the
aria-describedby wiring, and the controls inside it pick that wiring up through context.
The pieces
| Component | Use it for |
|---|---|
| Field | The wrapper. Generates ids, links label, control, help, and error. |
| Label | The control's label. Resolves htmlFor from Field context. |
| Input | A single-line text input. |
| TextArea | A multi-line text input. |
| NumberInput | An Input pinned to type="number". |
| PasswordInput | An Input with an obscure-text toggle. |
| FieldHelp | Supporting copy below the control. |
| FieldError | An error message with role="alert". |
| Fieldset | Groups related fields under a legend. |
Field does the wiring
A form control on its own is just an <input>. The accessible relationships — the label points at
the control, the control names its help and error text through aria-describedby, the invalid
state surfaces as aria-invalid — are what take work.
Field does that work. It generates a stable id, derives a helpId and an errorId from it, and
exposes them through React context. Every control inside a Field reads the context and wires
itself up: Label resolves its htmlFor, Input claims the id and the aria-describedby list,
FieldHelp and FieldError claim their ids. You write the markup; Field connects it.
The controls also work standalone. Drop an <Input> outside a Field and it renders fine — it
simply has no context to read, so the wiring is yours to supply.
State flows down
invalid, disabled, and required are Field-level props. Set them on the Field and every
control inside inherits them — the Input picks up aria-invalid, the Label renders its required
marker, the controls grey out. A control can still override its own state locally; the Field value
is a default, not a lock.