<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Form

A form that validates with the browser's own constraints (`required`, `type="email"`, `pattern`) and shows server errors on the matching fields. Pass `errors` straight from your action; each message lands on the field with that `name`, and focus moves to the first field with an error. Bind an accepted action answer with the framework-free `formErrors` constructor in `@/lib/ouro/form-answer`. Server errors only show: unlike the browser's checks they never block the next submit, so the user can always send the form again. A Form submits only itself: its submit event stops there, so a Form in a sheet inside another Form never submits the outer one. It renders a `<form>`, so `action` and `method` pass through; to submit through React Router, render its `Form`: `render={<RouterForm />}`.

```tsx
import { Form } from "@/components/ouro/form/form";
```

## Example

```tsx
import { formErrors } from "@/lib/ouro/form-answer";
const actionData = useActionData<typeof action>();
<Form method="post" errors={formErrors(actionData, actionData?.errors)} render={<RouterForm />}>
  <TextField label="Email" name="email" type="email" required />
  <Button type="submit">Save</Button>
</Form>
// The action answers { errors: { general, email } }: map its keys to this form's field names.
const errors = actionData?.errors;
<Form method="post" errors={formErrors(actionData, { form: errors?.general, email: errors?.email })} render={<RouterForm />}>
  <TextField label="Email" name="email" type="email" required />
  <Button type="submit">Subscribe</Button>
</Form>
```

## Usage

A submit is checked in this order. First the browser's own checks (`required`, `type="email"`, `pattern`): a failure stops the submit, marks each failing field with the browser's message and moves focus to the first. Then your `onSubmit`, which runs only when those pass and can still call `event.preventDefault()`. Then the server, whose answer comes back as `errors`.

The keys of `errors` are your fields' `name`s, and `form` is the message about the whole form. An action that answers with other keys is mapped where you pass them: `errors={formErrors(data, { form: data?.errors?.general, email: data?.errors?.email })}`. A key that no field in this form is named shows at the top of the form and warns in development with the key, this form's field names and the fix; with two forms on one route, pass each only its own keys.

`formErrors(answer, messages)` separates arrival from presentation. Same accepted answer: the clear mask persists, even through empty/omitted maps or a temporarily absent answer, and the latest uncleared messages render without new focus or a forced alert replacement. New accepted answer: reset the mask and create one arrival, even when messages are equal. Cached/reused answers require `{ key: acceptedAnswerKey }`, advanced only on actual acceptance. Form never guesses arrival from submit start, waits or framework navigation. The caller chooses accepted, form-local data: to give two Forms on one route answers of their own, pass each its own source.

Plain `errors={{ email: "Taken." }}` remains compatible and content-only: equal raw maps cannot prove a new answer. Use the constructor from the initial render for action answers; switching representations seeds a baseline and preserves clears rather than inventing arrival. Opaque identity does not survive JSON, spread, structured cloning or server component transport. Reconstruct beside Form in each environment with the locally accepted object or a safe explicit acceptance key; the returned value never retains or serializes the unselected source payload. Changed text in an existing alert may still produce ordinary live-region speech, but presentation is not a new answer generation.

The message about the whole form is an alert (`role="alert"`). It is in the page only while there is a message, and each answer adds a new one with its text already in it, which is what screen readers announce; `getByRole("alert")` finds nothing on a form without one. A field's message is not an alert: it is that control's description, so assert it with `toHaveAccessibleDescription`.

Focus moves to the first field with an error when errors arrive after a submit. A page that loads with `errors` already set (a post without JavaScript, or a reload) shows them in the server HTML and moves no focus: nothing was submitted from this page, and taking focus on load would drop a keyboard or screen-reader user into the middle of a page they have not read. To send them to the errors on load, put an in-page link to the form in your page's own error summary.

## Use instead when

- Use `EditSheet` instead for a form in a dialog that saves and closes, and a plain `<form>` when nothing needs validating.

## Props

Type: `FormProps`.

| Prop | Type | Description |
| --- | --- | --- |
| `children` (required) | `ReactNode` | The fields and the submit button. |
| `className` | `string` | Layout only: margins, width, grid placement. Fields stack with a gap by default; appearance classes warn in development. |
| `errors` | `ReadonlyFormErrors \| FormErrorValue \| null` | Errors by field name. Bind an accepted server answer inside this single prop: `errors={formErrors(actionData, actionData?.errors)}` (import `formErrors` from `@/lib/ouro/form-answer`). Each changed answer, including an equal refusal, restores cleared messages, replaces its alert and moves focus once. Rebuilding the value or its mapped messages with the same answer never counts as arrival. Each one shows on the field with that `name`, which gets `aria-invalid` and links the message, in the server HTML too. A field's own `error` prop wins over it, and a message goes away once the user changes that field. When new errors arrive, focus moves to the first field with one, in page order. The `form` key shows in an alert at the top of the form, in the server HTML too; a key no rendered field is named joins it once the page hydrates, and warns in development. A message is text or a React node (a message with a link), and an array is several messages. With `formErrors`, a field stays cleared for that answer even if its key disappears and is re-added. Uncleared messages use the latest projection, including new React nodes/handlers, without resetting the mask, forcing an alert remount or moving focus. No answer (`null`/`undefined`) is not arrival and preserves the established mask. Cached/reused or primitive answers need `{ key: acceptedAnswerKey }`, advanced on actual acceptance, never submit start. Select this form's accepted source yourself; Form does not infer ownership or cancellation from a router's global data. Plain records still work for display/local errors and are compared by content. Fresh equal records cannot identify equal new answers; use `formErrors` for action answers, including the initial undefined-answer render. Changing between raw and answer-bearing values establishes a baseline with the existing mask, not an arrival. Construct the value beside Form on server and client. JSON/spread/structured cloning or server-component transport loses opaque identity and leaves a plain message record; reconstruct using the locally accepted source or a safely transported acceptance key. These errors only show: they never block the next submit, so the user can always send the form again (a server error may be fixed by changing another field, or by trying again). The browser's own checks (`required`, `type="email"`) still block a submit. |

## Native attributes

Also accepts the attributes in React's `FormHTMLAttributes` and `HTMLAttributes` (`id`, `aria-*`, event handlers), passed to the rendered element, except those named on this page.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `result`: Not a prop. Bind the accepted answer inside `errors`: `errors={formErrors(answer, messages)}`.
- `validationErrors`: Not a prop. Ouro UI uses `errors`: `<Form errors={actionData?.errors}>`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `ErrorMessage`

One error message: text, or a React element for a message that carries a link or other markup.

```ts
type ErrorMessage = string | ReactElement;
```

### `FormErrorValue`

A read-only message snapshot with private, in-memory answer identity. Create it with `formErrors`; JSON, spread, structured cloning and server-component transport retain only the messages, not its identity. Reconstruct it beside Form on the server and in the browser. Array containers are snapshotted and frozen; React elements are retained, not cloned. React nodes are render inputs, not a promise that arbitrary node graphs JSON-serialize. Snapshot arrays are readonly, so use this type or `ReadonlyFormErrors`, not a mutable `FormErrors` annotation. This is not deep immutability of React nodes. TypeScript's `Array.isArray` narrows to `any[]` and can bypass readonly; runtime freezing still rejects mutation.

```ts
type FormErrorValue = ReadonlyFormErrors & { readonly [errorValueBrand]: true };
```

### `ReadonlyErrorMessages`

A read-only message view accepted by fields and Form; mutable input arrays remain valid.

```ts
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];
```

### `ReadonlyFormErrors`

A read-only display projection for Form and EditSheet; mutable raw `FormErrors` remain valid inputs.

```ts
type ReadonlyFormErrors = Readonly<Record<string, ReadonlyErrorMessages | null | undefined>>;
```
