> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Form

> API reference for form() — type-safe form handling with validation

`form()` creates a type-safe form instance from any `SdkMethod` — whether it's a [generated SDK](/api-reference/fetch/sdk) mutation, a [service action](/guides/server/services), or a custom endpoint. It provides reactive field state, schema validation, progressive enhancement attributes (`action`, `method`), and per-field error tracking.

When the SDK method carries a validation schema in its metadata (as generated methods do), `form()` picks it up automatically. For custom methods without metadata, pass a `schema` in the options. The `Result` type is handled internally — when the method returns `{ ok: false }`, field errors are extracted and mapped to the form fields.

## form()

```tsx theme={null}
import { form } from '@vertz/ui/form';

function CreateTaskForm({ onSuccess }: { onSuccess: () => void }) {
  const taskForm = form(api.tasks.create, {
    onSuccess,
  });

  return (
    <form action={taskForm.action} method={taskForm.method} onSubmit={taskForm.onSubmit}>
      <input name={taskForm.fields.title} />
      {taskForm.title.error && <span>{taskForm.title.error}</span>}

      <textarea name={taskForm.fields.description} />
      {taskForm.description.error && <span>{taskForm.description.error}</span>}

      <button type="submit" disabled={taskForm.submitting}>
        Create
      </button>
    </form>
  );
}
```

### Signature

```ts theme={null}
function form<TBody, TResult>(
  sdkMethod: SdkMethod<TBody, TResult>,
  options?: FormOptions<TBody, TResult>,
): FormInstance<TBody, TResult>;
```

***

## FormInstance

The returned instance has two kinds of properties: reserved form properties and per-field accessors.

### Form properties

| Property          | Type                                                 | Description                                                          |
| ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------- |
| `action`          | `string`                                             | URL for the form's `action` attribute (progressive enhancement)      |
| `method`          | `string`                                             | HTTP method for the form's `method` attribute                        |
| `onSubmit`        | `(e: Event) => Promise<void>`                        | Submit handler — validates, then calls the SDK method                |
| `submitting`      | `boolean`                                            | `true` while the submission is in flight                             |
| `dirty`           | `boolean`                                            | `true` if any field has been modified                                |
| `valid`           | `boolean`                                            | `true` if all fields pass validation                                 |
| `fields`          | `FieldNames<TBody>`                                  | Object mapping each field name to itself — use for `name` attributes |
| `reset()`         | `() => void`                                         | Reset all fields to initial values                                   |
| `submit()`        | `(formData?: FormData) => Promise<void>`             | Programmatic submit                                                  |
| `setFieldError()` | `(field: FieldPath<TBody>, message: string) => void` | Set a custom error on a field (supports dot-paths for nested fields) |

### Per-field accessors

Access any field by name on the form instance (e.g., `taskForm.title`). Each field is a `FieldState`:

| Property     | Type                  | Description                                       |
| ------------ | --------------------- | ------------------------------------------------- |
| `value`      | `T`                   | Current field value                               |
| `error`      | `string \| undefined` | Validation error message                          |
| `dirty`      | `boolean`             | `true` if the field has been modified             |
| `touched`    | `boolean`             | `true` if the field has been focused and blurred  |
| `setValue()` | `(value: T) => void`  | Update value and mark dirty                       |
| `reset()`    | `() => void`          | Reset to initial value, clear error/dirty/touched |

***

## FormOptions

| Option           | Type                                             | Default  | Description                                                        |
| ---------------- | ------------------------------------------------ | -------- | ------------------------------------------------------------------ |
| `schema`         | `FormSchema<TBody>`                              | —        | Validation schema (auto-inferred from SDK metadata when available) |
| `initial`        | `DeepPartial<TBody> \| () => DeepPartial<TBody>` | —        | Initial field values (supports nested objects)                     |
| `onSuccess`      | `(result: TResult) => void`                      | —        | Callback after successful submission                               |
| `onError`        | `(errors: Record<string, string>) => void`       | —        | Callback on validation or submission error                         |
| `resetOnSuccess` | `boolean`                                        | `false`  | Reset fields after successful submission                           |
| `revalidateOn`   | `'blur' \| 'change' \| 'submit'`                 | `'blur'` | When to re-validate fields with errors after first submission      |

***

## FormData coercion

Before validation, `form()` coerces each `FormData` string into the type declared by the body schema:

* `s.boolean()` — checkbox checked → `true`; absent (unchecked) → `false`; explicit `"false"`/`"0"`/`"off"` → `false`.
* `s.number()` / `s.bigint()` — numeric strings → numbers; empty strings dropped so the field is treated as missing (lets `optional()` validate).
* `s.date()` — parseable strings → `Date`.
* `s.string()` — never coerced (even when the value looks numeric).
* `s.array(<primitive>)` — multiple values for the same `name` produce an array of the leaf type.

Use plain `s.boolean()` / `s.number()` / `s.date()` in form schemas — `s.coerce.*` is not required. See the [Forms guide — FormData coercion](/guides/ui/forms#formdata-coercion) for the full table.

***

## Progressive enhancement

The `action` and `method` properties enable the form to work without JavaScript. When JS is available, `onSubmit` intercepts the submission and handles it client-side with validation.

```tsx theme={null}
<form action={taskForm.action} method={taskForm.method} onSubmit={taskForm.onSubmit}>
  {/* Works with and without JS */}
</form>
```

***

## Form-level onChange

For search/filter forms that need to react to every input change without submission, use `<form onChange>` instead of (or alongside) `form()`. The handler receives a `FormValues` snapshot of all current form values, and individual inputs can use `debounce={ms}` to delay the callback.

See the [Forms guide — Form-level onChange](/guides/ui/forms#form-level-onchange) for usage, debounce, and examples.

***

## Types

```ts theme={null}
interface FormOptions<TBody, TResult> {
  schema?: FormSchema<TBody>;
  initial?: DeepPartial<TBody> | (() => DeepPartial<TBody>);
  onSuccess?: (result: TResult) => void;
  onError?: (errors: Record<string, string>) => void;
  resetOnSuccess?: boolean;
  revalidateOn?: 'blur' | 'change' | 'submit';
}

// Types below show the developer-facing API — the compiler auto-unwraps
// reactive properties, so you use them as plain values in JSX and templates.
type FormInstance<TBody, TResult> = FormBaseProperties<TBody> & NestedFieldAccessors<TBody>;

interface FormBaseProperties<TBody> {
  action: string;
  method: string;
  onSubmit: (e: Event) => Promise<void>;
  reset: () => void;
  setFieldError: (field: FieldPath<TBody>, message: string) => void;
  submit: (formData?: FormData) => Promise<void>;
  submitting: boolean;
  dirty: boolean;
  valid: boolean;
  fields: FieldNames<TBody>;
}

interface FieldState<T = unknown> {
  value: T;
  error: string | undefined;
  dirty: boolean;
  touched: boolean;
  setValue: (value: T) => void;
  reset: () => void;
}

// Recursive field accessors — nested objects get both FieldState
// and nested accessors (e.g., `form.address.city.error`).
// Simplified — the real type also handles arrays (numeric index access),
// built-in objects (Date, File, Blob treated as leaves), and detects
// reserved field name conflicts at compile time.
type NestedFieldAccessors<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? FieldState<T[K]> & NestedFieldAccessors<T[K]>
    : FieldState<T[K]>;
};

// Recursive dot-path type for nested fields (e.g., 'address.city')
type FieldPath<T, Prefix extends string = ''> =
  | `${Prefix}${keyof T & string}`
  | {
      [K in keyof T & string]: T[K] extends Record<string, unknown>
        ? FieldPath<T[K], `${Prefix}${K}.`>
        : never;
    }[keyof T & string];

// Maps each field name to itself — for type-safe `name` attributes
type FieldNames<TBody> = { readonly [K in keyof TBody & string]: K };

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T[K] extends Record<string, unknown>
      ? DeepPartial<T[K]>
      : T[K];
};

interface FormSchema<T> {
  parse(data: unknown): { ok: true; data: T } | { ok: false; error: unknown };
}
```
