Form, Field & useFormState
Everything returned by createForm — the Form wrapper, the Field component, and the hook for reading submission state.
Form
Form is a <form> wrapper that handles validation and submission. It provides context for all <Field> components and the useFormState hook nested inside it.
| Prop | Type | Description |
|---|---|---|
| className | string? | CSS class applied to the underlying <form> element. |
| children | ReactNode? | Optional. Can contain <Field> overrides and any other elements (buttons, headings, dividers). Non-Field children are rendered after the auto-generated field rows. |
With an adapter
When an adapter is configured, every schema key becomes an auto-rendered field. An empty <Form> with just a submit button is all you need:
const { Form, useFormState } = createForm({
schema,
adapter: shadcnAdapter,
action,
defaultValues: { username: "", email: "" },
});
const SignupForm = () => {
const { isSubmitting } = useFormState();
return (
<Form className="space-y-4">
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Signing up..." : "Sign up"}
</button>
</Form>
);
};Without an adapter
Without an adapter, <Form> requires explicit <Field render={...}> children for every field — there is nothing to auto-render. TypeScript enforces this: omitting render is a compile error.
const { Form, Field } = createForm({ schema, action }); // no adapter
<Form>
<Field
name="username"
render={({ field }) => (
<input value={field.value as string} onChange={field.onChange} />
)}
/>
<button type="submit">Submit</button>
</Form>Field
Field renders a single form field. When placed inside a <Form>, it overrides the auto-generated version for that schema key. The name prop is typed against your schema — TypeScript will catch typos or missing keys at compile time.
| Prop | Required | Description |
|---|---|---|
| name | always | The schema key this field maps to. Typed as a union of your schema's top-level keys — invalid names are a TypeScript error. |
| label | optional | Display label for this field. Overrides the auto-generated label. If omitted, the label is derived from the field name — firstName becomes First Name. |
| render | required without adapter/ optional with adapter | Custom render function. Receives a fieldobject and returns JSX. When provided, it replaces the adapter's default widget for this field. |
| disabled | optional | Passes the disabled flag to the rendered input. The adapter and the render prop both receive it. |
| ErrorElement | optional | Custom component for displaying this field's error message. Overrides GlobalErrorElement for this field only. Receives errorMessage: string. |
The render prop
The render function receives a field object with everything you need to wire any input:
| Property | Description |
|---|---|
| name | The field key string, e.g. "username". |
| value | Current field value, typed to the field's Zod output type. |
| onChange | Call with a new value or a DOM ChangeEvent — Formura extracts the value from either. For checkboxes, pass a boolean. |
| onBlur | Blur handler. Reserved for future use — safe to forward. |
| disabled | Boolean from the disabled prop. |
<Field
name="bio"
label="About you"
render={({ field }) => (
<textarea
value={field.value as string}
onChange={field.onChange}
disabled={field.disabled}
rows={4}
/>
)}
/>Overriding one field
Place a <Field> inside the form when you want to override only one or two fields while leaving the rest auto-rendered by the adapter:
<Form className="space-y-4">
{/* This field is customised — the rest auto-render normally */}
<Field
name="avatar"
label="Profile photo"
render={({ field }) => <AvatarUpload onChange={field.onChange} />}
/>
<button type="submit">Save</button>
</Form>useFormState
useFormState is a hook that exposes live submission state. It reads from the same context that <Form> provides — it must be called inside a component that is rendered within that <Form> tree.
| Property | Type | Description |
|---|---|---|
| isSubmitting | boolean | true while the action is in flight. Goes back to false when the action resolves — or immediately if client-side validation fails (action was never called). |
| result | ActionResult | null | The last value returned by your action. null before the first submission. Contains status: "success" or status: "error" plus optional data or message. |
| submitCount | number | Increments on every submit attempt, including ones that fail client-side validation. Useful for showing hints after multiple failed attempts. |
const { Form, useFormState } = createForm({ schema, adapter, action });
const SignupForm = () => {
const { isSubmitting, result, submitCount } = useFormState();
return (
<Form className="space-y-4">
{result?.status === "success" && (
<p className="text-green-500">Account created!</p>
)}
{result?.status === "error" && result.message && (
<p className="text-red-500">{result.message}</p>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Signing up..." : "Sign up"}
</button>
{submitCount > 2 && result?.status !== "success" && (
<p className="text-sm text-muted-foreground">
Having trouble? Check your input or try a different username.
</p>
)}
</Form>
);
};useFormState can be called in any component rendered inside <Form> — it does not have to be in the same component that renders the form itself. This makes it easy to extract a submit button or a result banner into a separate component.
Override precedence
When multiple sources configure the same field, they merge from lowest to highest priority:
Auto-generated
Label derived from the field name, no overrides. This is the default when nothing else is specified.
fields option in createForm
Per-field config passed when calling createForm. Useful when the form factory lives in a separate module from the component.
<Field> JSX child
Explicit <Field name="..." /> inside the Form tree. Always wins over everything else for that field.
createForm({
schema,
adapter: shadcnAdapter,
action,
fields: {
username: { label: "Handle" }, // priority 2 — overrides auto "Username"
},
});
<Form>
{/* priority 3 — wins over everything */}
<Field name="username" label="Your unique handle" />
<button type="submit">Submit</button>
</Form>Error display
Field errors come from two sources: schema validation on submit, and fieldErrors returned by your action. Both map to the same per-field error state — your UI does not need to distinguish between them.
By default the adapter renders errors in its own style. You can replace the error display globally or per-field:
const MyError = ({ errorMessage }: { errorMessage: string }) => (
<p className="mt-1 text-xs text-red-500">{errorMessage}</p>
);
createForm({
schema,
adapter: shadcnAdapter,
action,
GlobalErrorElement: MyError, // default for all fields
fields: {
email: { ErrorElement: MyError }, // overrides GlobalErrorElement for email only
},
});Priority: Field prop ErrorElement > field config ErrorElement > GlobalErrorElement> adapter default.
Next
Widgets
How Zod types map to text, select, checkbox, date pickers, and more.