opsinjs
HandbookWorking with components

Forms

Wiring a Field to React Hook Form, TanStack Form or a native form. The validation boundary, server errors, and the accessibility the wiring must not lose.

The short version

NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.

This component is not built yet

There is nothing to render because there is nothing to install. What you can read on this page is the specification the implementation will have to satisfy.

PlannedRoadmapWhat “planned” means

Field will own the accessible plumbing, and your form library will own the state and the validation. The plumbing is the label association, the description, the error message, aria-invalid and aria-describedby. The split is deliberate: the parts that are easy to get subtly wrong are in the component, and the parts that are a matter of preference are yours.

Whichever library you use, three things must remain true when you are finished:

  1. The error message is associated with its control, not merely near it.
  2. aria-invalid appears and disappears with the error.
  3. A failed submit produces a focused error summary. See Error summaries.

This page is the mechanics. When validation should run, and what the form should look like, are design decisions and live in Form design.

How it works

Native forms

A form with no library at all is a legitimate choice, and for a single-question page it is usually the right one. The browser's own constraint validation gives you required, min, max, pattern and type checking for free; what it does not give you is control over when the message appears or what it says, so you will normally suppress the native bubble with noValidate and render the message yourself.

React Hook Form

The uncontrolled model registers the input and reads values at submit. It is the cheapest option for a large form because it does not re-render on every keystroke. The integration points are the ref (which is why a wrapper that drops it breaks everything, see Composition and render), the field name, and the error object you map onto the Field's error slot.

TanStack Form

The field-level subscription model suits forms whose fields depend on each other, where a change in one has to revalidate another. The integration shape is the same: the library owns the value and the error, Field owns the markup and the ARIA.

Server errors

Errors returned from a submit are not a different kind of error to the reader and must not look like one. Map them back onto the same fields, into the same error slots, and into the same summary. When a form shows one error presentation for client validation and another for the server, it teaches the reader that the second kind is a system failure rather than something they can fix.

Do this

  • Give every control a stable id and let Field wire the label and description to it. The error summary needs those ids to link to.
  • Set aria-invalid from the error state, and clear it when the error clears. A control that stays invalid after correction is a common and confusing residue.
  • Validate on blur first, live on correction, always on submit. The reasoning is in Validation timing; the implementation is a mode setting in most libraries.
  • Keep the submit control enabled. Disabling it until the form is valid removes the reader's only way to find out what is wrong, and a disabled button is not reachable by keyboard in most implementations.
  • Preserve values across a failed submit and across back-navigation. WCAG 2.2 SC 3.3.7.
  • Type your form values. A schema-derived type catches the field-name typo that otherwise silently validates nothing. See TypeScript.

Not this

  • Do not use a placeholder as a label. It disappears on entry, it usually fails contrast, and it is not an accessible name. WCAG 2.2 SC 3.3.2.
  • Do not render the error message as a sibling with no association. Visually adjacent is not programmatically determinable; without aria-describedby a screen-reader user reaching the field hears nothing about the error.
  • Do not block a save on a health value being implausible. The reason is in Validation timing and it is the most consequential rule on this page.
  • Do not validate on every keystroke by default. It is the default in several libraries and it is hostile to slow typists and to screen-reader users.
  • Do not reset the form on a server error. The reader's work is not the server's to discard.
  • Do not disable paste, anywhere, for any reason.

Gotchas

  • A wrapper that drops ref breaks React Hook Form silently. No error; the field is simply never registered and its value is always undefined. This is the single most common integration failure.
  • Controlled and uncontrolled is a one-way door per field. Switching a component between the two mid-life produces a React warning and, more annoyingly, a caret that jumps to the end of the input.
  • aria-describedby is a single attribute with a space-separated list. Setting it for the error overwrites the hint unless you compose both ids yourself; Field composes them for you, which is why you should not set it by hand.
  • A native required plus a visible "(required)" in the label announces twice. Under the marked-optional convention this does not arise; see Required and optional.
  • type="number" and controlled state disagree about intermediate values. A field containing 1. reports an empty value, so a controlled component clears it as the reader types. Use type="text" with inputmode="decimal". See Autocomplete and input types.
  • Async validation races. A slow check that resolves after the reader has corrected the field will re-apply a stale error unless you key the result to the value it was computed from.
  • Form design has the behavioural decisions this page implements.
  • Field is the component contract: parts, states and accessible naming.
  • Error summaries says what a failed submit must do.
  • Ask users for… covers per-field guidance for the data a health product actually collects.

On this page