A form is a list of fields, a legend, one submit control and a rule for what counts as an
error. The framework supplies the controls and the states; the page supplies the values and
the rules. This page is the shape that works, in the order the pieces matter.

## The parts

| Part | Component | Why it is a component rather than markup |
| --- | --- | --- |
| The form | [`Form`](/components/forms/form) | It owns the legend, the submit control and the pending state, so a form cannot ship without them |
| The fields | the [field family](/concepts/fields) | Each renders its label, description, control and message as one unit |
| The steps | [`Stepper`](/components/forms/stepper) | Progress, when the form is long enough to need it |

```tsx demo="guides/form-complete"
```

## Values belong to the page

Fields are controlled: the page holds the value and the field reports what happened. That is
what lets a page validate, reset, prefill or submit without reaching into the DOM.

```tsx
const [email, setEmail] = useState("");

<Input
  label="Email"
  value={email}
  onChangeText={setEmail}
  status={emailError ? "error" : "default"}
  message={emailError}
/>
```

An uncontrolled field keeps its own value, useful for a one-off filter, but a form that
submits needs the values, so a form's fields are usually controlled.

## Validation is a status and a message

There is no second error channel. A field's `message` carries the problem and its `status`
carries the meaning; an `"error"` message is announced immediately, because a validation
failure the reader has to go looking for is one they will miss.

The useful order is:

1. **On submit**, validate everything and show every message at once, not one per attempt.
2. **On change after that**, re-validate the field the reader is editing, so a fixed field
   clears its message as they fix it.
3. **On blur**, optionally validate a single field (`email`, `url`, a code) where the answer
   is already known.

```tsx
const [errors, setErrors] = useState<Record<string, string>>({});

function submit(event) {
  event.preventDefault();
  const found = validate(values);
  setErrors(found);
  if (Object.keys(found).length > 0) return;   // keep the page where it is
  send(values);
}
```

## Submission and the pending state

`Form` renders the submit control and takes `isPending`. While it is pending the control is
busy and cannot be pressed twice, and the label stays put so the row does not move, the
same rule [`Button`](/components/forms/button) follows for its `isLoading`.

```tsx demo="guides/form-pending"
```

Keep the page where it is while submitting: do not navigate before the request resolves, and
do not clear the fields until it succeeds. A form that empties itself on failure has thrown
away the reader's work.

## Focus

After a failed submit, move focus to the first field with an error. The framework announces
the message; it cannot know which field the reader should fix first. A page does that with a
ref on the first invalid control:

```tsx
useEffect(() => {
  if (!submitted) return;
  const first = Object.keys(errors)[0];
  if (first) document.getElementById(first)?.focus();
}, [submitted, errors]);
```

Two smaller rules: the submit control is a real submit button inside the form's element, so
<kbd>Enter</kbd> in a single-line field submits; and a field that is disabled mid-request is
re-enabled in a `finally`, so a rejected request does not leave the form frozen.

## Accessibility checklist

- Every field has a visible `label`. A hidden label is still rendered and referenced, it is
  never omitted.
- The message is a `message` prop, not a tooltip and not a colour: it is announced, and it
  survives a screenshot.
- The required marker is decorative; the requirement is on the control.
- Errors are announced when they appear, and focus moves to the first one.
- The form has a `legend`, a group of fields without a name is a group a screen reader
  cannot announce.
