snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

View File

@@ -0,0 +1,678 @@
---
title: Formisch
description: Build forms in React using Formisch and Valibot.
links:
doc: https://formisch.dev
---
import { InfoIcon } from "lucide-react"
This guide covers building forms with [Formisch](https://formisch.dev), the lightweight, schema-first, and fully type-safe form library for React. We'll create forms with the `<Field />` component, validate them with Valibot schemas, handle errors, and ensure accessibility.
## Demo
We'll build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in Formisch. It
is recommended to add basic browser validation in your production code.
</Callout>
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-demo"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages Formisch for headless, schema-first form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses Formisch's `useForm` hook for form state management.
- `<Form />` component to wrap the native `<form>` element with submit handling.
- `<Field />` render-prop component for controlled inputs.
- Schema validation using [Valibot](https://valibot.dev).
- Type-safe field paths inferred from the schema.
## Form Methods
Formisch exposes form operations as **top-level functions** rather than methods on a form object. Import only what you need:
```ts
import { getInput, insert, reset, submit } from "@formisch/react"
```
Every method follows the same signature: the **first parameter is always the form store**, and the **second parameter (if necessary) is always a config object**.
```ts
// Read a field value
const email = getInput(form, { path: ["email"] })
// Reset the form with new initial values
reset(form, { initialInput: { email: "", password: "" } })
// Move an item in a field array
move(form, { path: ["items"], from: 0, to: 3 })
```
This design keeps the API flexible and consistent across all methods. You'll see the same `(form, config)` shape used throughout this guide for reading state (`getInput`, `getErrors`), writing state (`setInput`, `setErrors`), form control (`submit`, `validate`, `focus`), and array operations (`insert`, `remove`, `move`, `swap`, `replace`). See the [full methods reference](https://formisch.dev/react/guides/form-methods) for details.
## Anatomy
Here's a basic example of a form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
```tsx showLineNumbers {3-21}
<Form of={form} onSubmit={handleSubmit}>
<FieldGroup>
<FormischField of={form} path={["title"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-title">Bug Title</FieldLabel>
<Input
{...field.props}
id="form-title"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
</FieldGroup>
</Form>
```
<Callout icon={<InfoIcon />}>
**Note:** Formisch ships its own `Field` component. To avoid a name clash with
the shadcn `Field`, the examples below import the Formisch one as
`FormischField` and keep the shadcn `Field` under its original name. In your
own code you can alias either side — just be consistent.
</Callout>
## Form
### Create a form schema
We'll start by defining the shape of our form using a Valibot schema. Formisch infers all input and output types directly from this schema.
```tsx showLineNumbers title="form.tsx"
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
})
```
### Set up the form
Next, we'll use the `useForm` hook from Formisch to create our form instance. The schema is passed directly to `useForm` — there is no resolver step.
```tsx showLineNumbers title="form.tsx" {1-2,21-25}
import { Form, Field as FormischField, useForm } from "@formisch/react"
import type { SubmitHandler } from "@formisch/react"
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
})
export function BugReportForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
// Do something with the validated form values.
console.log(output)
}
return (
<Form of={form} onSubmit={handleSubmit}>
{/* ... */}
{/* Build the form here */}
{/* ... */}
</Form>
)
}
```
The `<Form />` component wraps a native `<form>` element. It calls `event.preventDefault()`, runs validation, and only invokes `onSubmit` when the data is valid. The `output` you receive is fully typed from the schema.
### Build the form
We can now build the form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-formisch-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `handleSubmit` function will be called with the validated form data. If the form data is invalid, Formisch will populate `field.errors` for each invalid field and the UI will display them.
## Validation
### Client-side Validation
Formisch validates your form data using the Valibot schema you pass to `useForm`. There is no resolver — the schema is the single source of truth for both runtime validation and static types.
```tsx showLineNumbers title="form.tsx" {1,3-6,11}
import { useForm } from "@formisch/react"
const FormSchema = v.object({
title: v.string(),
description: v.optional(v.string()),
})
export function ExampleForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
}
```
### Validation Modes
Formisch separates the **first** validation from **subsequent** validations. You configure them with the `validate` and `revalidate` options on `useForm`.
```tsx showLineNumbers title="form.tsx" {3-4}
const form = useForm({
schema: FormSchema,
validate: "blur",
revalidate: "input",
})
```
| Option | Value | Description |
| ------------ | ----------- | --------------------------------------------------------------- |
| `validate` | `"submit"` | Validate on form submission (default). |
| `validate` | `"blur"` | Validate when a field loses focus. |
| `validate` | `"input"` | Validate on every input change. |
| `validate` | `"initial"` | Validate immediately on form creation. |
| `revalidate` | `"input"` | Revalidate on every input change after the first run (default). |
| `revalidate` | `"blur"` | Revalidate on blur after the first run. |
| `revalidate` | `"submit"` | Revalidate only on form submission. |
## Displaying Errors
Display errors next to the field using `<FieldError />`. Formisch returns errors as an array of strings, so map them to the shape `<FieldError />` expects. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {3,10,12-14}
<FormischField of={form} path={["email"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-email">Email</FieldLabel>
<Input
{...field.props}
id="form-email"
value={field.input}
type="email"
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
```
## Working with Different Field Types
Formisch exposes two ways to bind a field to an element:
- **Native HTML elements** (like `<Input />` and `<Textarea />`) — spread `field.props` and provide `value={field.input}`. Formisch wires up `name`, `ref`, `onChange`, `onBlur`, and `onFocus` for you.
- **Component-library inputs** (like Radix-based `<Select />`, `<Checkbox />`, `<RadioGroup />`, `<Switch />`) — read the value from `field.input` and call `field.onChange(value)` to update it.
### Input
- For input fields, spread `field.props` and provide `value={field.input}`.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-input"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5-8}
<FormischField of={form} path={["username"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-username">Username</FieldLabel>
<Input
{...field.props}
id="form-username"
value={field.input}
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
```
### Textarea
- For textarea fields, spread `field.props` and provide `value={field.input}`.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-textarea"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {7-10}
<FormischField of={form} path={["about"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-about">More about you</FieldLabel>
<Textarea
{...field.props}
id="form-about"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
```
### Select
- For select components, read `field.input` and call `field.onChange` from `<Select />`'s `onValueChange`.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-select"
className="sm:[&_.preview]:h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {15-19}
<FormischField of={form} path={["language"]}>
{(field) => (
<Field orientation="responsive" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-language">Spoken Language</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Select value={field.input} onValueChange={field.onChange}>
<SelectTrigger
id="form-language"
aria-invalid={field.errors !== null}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
</FormischField>
```
### Checkbox
- For checkbox arrays, read `field.input` and update it from `onCheckedChange` using `field.onChange`.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-checkbox"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {16,19-25}
<FormischField of={form} path={["tasks"]}>
{(field) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={field.errors !== null}
>
<Checkbox
id={`form-checkbox-${task.id}`}
aria-invalid={field.errors !== null}
checked={field.input?.includes(task.id) ?? false}
onCheckedChange={(checked) => {
const current = field.input ?? []
field.onChange(
checked === true
? [...current, task.id]
: current.filter((value) => value !== task.id)
)
}}
/>
<FieldLabel
htmlFor={`form-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>
```
### Radio Group
- For radio groups, read `field.input` and call `field.onChange` from `onValueChange`.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-radiogroup"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {9-13,21}
<FormischField of={form} path={["plan"]}>
{(field) => (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup value={field.input} onValueChange={field.onChange}>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
<Field
orientation="horizontal"
data-invalid={field.errors !== null}
>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-radiogroup-${plan.id}`}
aria-invalid={field.errors !== null}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>
```
### Switch
- For switches, read `field.input` and call `field.onChange` from `onCheckedChange`.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-switch"
className="sm:[&_.preview]:h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {15-19}
<FormischField of={form} path={["twoFactor"]}>
{(field) => (
<Field orientation="horizontal" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Switch
id="form-twoFactor"
checked={field.input ?? false}
onCheckedChange={field.onChange}
aria-invalid={field.errors !== null}
/>
</Field>
)}
</FormischField>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-complex"
className="sm:[&_.preview]:h-[1300px]"
chromeLessOnMobile
/>
## Resetting the Form
Formisch exposes a top-level `reset` function. Pass the form store to reset it to its initial input.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => reset(form)}>
Reset
</Button>
```
You can also reset to new initial values, or reset while keeping the user's current input:
```tsx showLineNumbers
// Reset to a fresh set of initial values
reset(form, { initialInput: { title: "", description: "" } })
// Sync the baseline to new server data, but keep the user's edits
reset(form, { initialInput: serverData, keepInput: true })
```
## Array Fields
Formisch provides a `<FieldArray />` component and a set of helper functions for managing dynamic array fields. Use it whenever you need to add, remove, or reorder items.
<ComponentPreview
styleName="new-york-v4"
name="form-formisch-array"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
### Using FieldArray
`<FieldArray />` follows the same render-prop pattern as `<Field />`. Its `items` array contains a stable key per item that you should use as the React `key`.
```tsx showLineNumbers title="form.tsx" {1,7-22}
import {
Field as FormischField,
FieldArray,
insert,
remove,
} from "@formisch/react"
export function ExampleForm() {
// ... form config
return (
<FieldArray of={form} path={["emails"]}>
{(fieldArray) => (
<FieldGroup className="gap-4">
{fieldArray.items.map((item, index) => (
<FormischField
key={item}
of={form}
path={["emails", index, "address"]}
>
{(field) => /* ... */}
</FormischField>
))}
</FieldGroup>
)}
</FieldArray>
)
}
```
### Array Field Structure
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
```tsx showLineNumbers title="form.tsx"
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>
```
### Adding Items
Use the `insert` function to add new items to the array. By default new items are appended to the end. You can also pass an `at` index to insert at a specific position.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
insert(form, { path: ["emails"], initialInput: { address: "" } })
}
disabled={fieldArray.items.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use the `remove` function with an `at` index to remove items from the array.
```tsx showLineNumbers title="form.tsx"
import { remove } from "@formisch/react"
{
fieldArray.items.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(form, { path: ["emails"], at: index })}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}
```
Formisch also exposes `move`, `swap`, and `replace` for reordering and replacing items. They follow the same `(form, config)` signature.
### Array Validation
Use Valibot's `array` and pipeline validators to constrain array fields.
```tsx showLineNumbers title="form.tsx"
const FormSchema = v.object({
emails: v.pipe(
v.array(
v.object({
address: v.pipe(
v.string(),
v.nonEmpty("Enter an email address."),
v.email("Enter a valid email address.")
),
})
),
v.minLength(1, "Add at least one email address."),
v.maxLength(5, "You can add up to 5 email addresses.")
),
})
```

View File

@@ -0,0 +1,60 @@
---
title: Forms
description: Build forms with React and shadcn/ui.
---
import { ClipboardListIcon } from "lucide-react"
## Pick Your Framework
Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn/ui and the form library of your choice.
<div className="mt-8 grid gap-4 sm:grid-cols-2 sm:gap-6">
<LinkedCard href="/docs/forms/react-hook-form">
<ClipboardListIcon className="size-10" />
<p className="mt-2 font-medium">React Hook Form</p>
</LinkedCard>
<LinkedCard href="/docs/forms/tanstack-form">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
>
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
</svg>
<p className="mt-2 font-medium">TanStack Form</p>
</LinkedCard>
<LinkedCard href="/docs/forms/formisch">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="size-10"
fill="currentColor"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="20"
d="M90.9 291.7 49 299.4v125.5l193.3 63.8 221-63.8V304.7l-62-13.7m-352.2 8.6 193.2 53.6V488m221-183.2-221.1 48.4m-171-67.7 147.5 47 212.7-48.3L437 76 271.9 25 72.1 66.3Zm1-219.3L220.5 118l-1.8 214m1.8-214.3 216.2-41.6m-84.4 159.6-.9 1.9c-5.2 11.3-16.2 34.8-33.5 35.5h-1a22 22 0 0 1-9.7-2.2 26 26 0 0 1-7.4-5.5c-4.3-4.4-7.4-10-10-14.5q-1.2-2.4-2.7-4.7a54 54 0 0 0 21.7 4.1 74 74 0 0 0 23.6-4 70 70 0 0 0 11.4-5 64 64 0 0 0 8.5-5.6M247.6 168c19.5-20.8 34.8-12.3 36-18.4 1.5-7-12.2-7.7-22.2-2.6s-18 17.7-13.8 21m101.2-33.2c1.6 2.6 8.7-1.6 19.5-1.2s15.8 5.7 18 4.1-4.6-14.7-17.1-15.2-22 9.7-20.4 12.3m21.2 16.3c9.4.6 17.1 12.8 16.1 27.6s-10.4 25.9-19.8 25.2-17.1-12.9-16.1-27.7 10.4-25.8 19.8-25.1m-97.4 19c11 1.2 19 15.1 17.4 30.5s-12.3 27.4-23.4 26.2-19-15-17.4-30.5 12.3-27.3 23.4-26.2"
/>
</svg>
<p className="mt-2 font-medium">Formisch</p>
</LinkedCard>
<LinkedCard href="#" className="border border-dashed bg-transparent">
<svg
role="img"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>React</title>
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
<p className="mt-2 font-medium">useActionState</p>
<p className="mt-1 text-xs text-muted-foreground">(Coming Soon)</p>
</LinkedCard>
</div>

View File

@@ -0,0 +1,3 @@
{
"pages": ["react-hook-form", "tanstack-form", "formisch"]
}

View File

@@ -0,0 +1,399 @@
---
title: Next.js
description: Build forms in React using useActionState and Server Actions.
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with Next.js using `useActionState` and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more.
## Demo
We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state.
<ComponentPreview
styleName="new-york-v4"
name="form-next-demo"
className="[&_.preview]:h-[700px] [&_pre]:h-[700px]!"
/>
<Callout icon={<InfoIcon />}>
**Note:** The examples on this page intentionally disable browser validation
to show how schema validation and form errors work in server actions.
</Callout>
## Approach
This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses Next.js `<Form />` component for navigation and progressive enhancement.
- `<Field />` components for building accessible forms.
- `useActionState` for managing form state and errors.
- Handles loading states with the pending prop.
- Server Actions for handling form submissions.
- Server-side validation using Zod.
## Anatomy
Here's a basic example of a form using the `<Field />` component.
```tsx showLineNumbers
<Form action={formAction}>
<FieldGroup>
<Field data-invalid={!!formState.errors?.title?.length}>
<FieldLabel htmlFor="title">Bug Title</FieldLabel>
<Input
id="title"
name="title"
defaultValue={formState.values.title}
disabled={pending}
aria-invalid={!!formState.errors?.title?.length}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{formState.errors?.title && (
<FieldError>{formState.errors.title[0]}</FieldError>
)}
</Field>
</FieldGroup>
<Button type="submit">Submit</Button>
</Form>
```
## Usage
### Create a form schema
We'll start by defining the shape of our form using a Zod schema in a `schema.ts` file.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other schema validation library. Make sure your schema
library conforms to the Standard Schema specification.
</Callout>
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Define the form state type
Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server.
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export type FormState = {
values?: z.infer<typeof formSchema>
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
success: boolean
}
```
**Important:** We define the schema and the `FormState` type in a separate file so we can import them into both the client and server components.
### Create the Server Action
A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo-action.ts"
title="actions.ts"
/>
**Note:** We're returning `values` for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form.
### Build the form
We can now build the form using the `<Field />` component. We'll use the `useActionState` hook to manage the form state, server action, and pending state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client and server-side validation.
When you submit the form, the `formAction` function will be called on the server. The server action will validate the form data and update the form state.
If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state.
## Pending States
Use the `pending` prop from `useActionState` to show loading indicators and disable form inputs.
```tsx showLineNumbers {11,26-34}
"use client"
import * as React from "react"
import Form from "next/form"
import { Spinner } from "@/components/ui/spinner"
import { bugReportFormAction } from "./actions"
export function BugReportForm() {
const [formState, formAction, pending] = React.useActionState(
bugReportFormAction,
{
errors: null,
success: false,
}
)
return (
<Form action={formAction}>
<FieldGroup>
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
<Field>
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
</Field>
</FieldGroup>
</Form>
)
}
```
## Disabled States
### Submit Button
To disable the submit button, use the `pending` prop on the button's `disabled` prop.
```tsx showLineNumbers
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
```
### Field
To apply a disabled state and styling to a `<Field />` component, use the `data-disabled` prop on the `<Field />` component.
```tsx showLineNumbers
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
```
## Validation
### Server-side Validation
Use `safeParse()` on your schema in your server action to validate the form data.
```tsx showLineNumbers title="actions.ts" {12-20}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
return {
errors: null,
success: true,
}
}
```
### Business Logic Validation
You can add additional custom validation logic in your server action.
Make sure to return the values on validation errors. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {22-35}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Check if email already exists in database.
const existingUser = await db.user.findUnique({
where: { email: result.data.email },
})
if (existingUser) {
return {
values,
success: false,
errors: {
email: ["This email is already registered"],
},
}
}
return {
errors: null,
success: true,
}
}
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. Make sure to add the `data-invalid` prop to the `<Field />` component and `aria-invalid` prop to the input.
```tsx showLineNumbers
<Field data-invalid={!!formState.errors?.email?.length}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
aria-invalid={!!formState.errors?.email?.length}
/>
{formState.errors?.email && (
<FieldError>{formState.errors.email[0]}</FieldError>
)}
</Field>
```
## Resetting the Form
When you submit a form with a server action, React will automatically reset the form state to the initial values.
### Reset on Success
To reset the form on success, you can omit the `values` from the server action and React will automatically reset the form state to the initial values. This is standard React behavior.
```tsx showLineNumbers title="actions.ts" {22-26}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Business logic.
callYourDatabaseOrAPI(values)
// Omit the values on success to reset the form state.
return {
errors: null,
success: true,
}
}
```
### Preserve on Validation Errors
To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {12-17}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
// Return the values on validation errors.
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
}
```
## Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
styleName="new-york-v4"
name="form-next-complex"
className="[&_.preview]:h-[1100px] [&_pre]:h-[1100px]!"
hideCode
/>
### Schema
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-schema.ts"
title="schema.ts"
/>
### Form
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex.tsx"
title="form.tsx"
/>
### Server Action
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-action.ts"
title="actions.ts"
/>

View File

@@ -0,0 +1,638 @@
---
title: React Hook Form
description: Build forms in React using React Hook Form and Zod.
links:
doc: https://react-hook-form.com
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `<Field />` component, adding schema validation using Zod, error handling, accessibility, and more.
## Demo
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in React Hook
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-demo"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses React Hook Form's `useForm` hook for form state management.
- `<Controller />` component for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod with `zodResolver`.
## Anatomy
Here's a basic example of a form using the `<Controller />` component from React Hook Form and the `<Field />` component.
```tsx showLineNumbers {5-18}
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Form
### Create a form schema
We'll start by defining the shape of our form using a Zod schema.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other Standard Schema validation library supported by
React Hook Form.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Set up the form
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
```tsx showLineNumbers title="form.tsx" {17-23}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export function BugReportForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
// Do something with the form values.
console.log(data)
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* ... */}
{/* Build the form here */}
{/* ... */}
</form>
)
}
```
### Build the form
We can now build the form using the `<Controller />` component from React Hook Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-rhf-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
## Validation
### Client-side Validation
React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook.
```tsx showLineNumbers title="example-form.tsx" {5-8,12}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z.string(),
description: z.string().optional(),
})
export function ExampleForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
}
```
### Validation Modes
React Hook Form supports different validation modes.
```tsx showLineNumbers title="form.tsx" {3}
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
})
```
| Mode | Description |
| ------------- | -------------------------------------------------------- |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit (default). |
| `"onTouched"` | Validation triggers on first blur, then on every change. |
| `"all"` | Validation triggers on blur and change. |
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {5,11,13}
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
id={field.name}
type="email"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Working with Different Field Types
### Input
- For input fields, spread the `field` object onto the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-input"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
For simple text inputs, spread the `field` object onto the input.
```tsx showLineNumbers title="form.tsx" {5,7,8}
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Textarea
- For textarea fields, spread the `field` object onto the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-textarea"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
For textarea fields, spread the `field` object onto the textarea.
```tsx showLineNumbers title="form.tsx" {5,10,18}
<Controller
name="about"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
<Textarea
{...field}
id="form-rhf-textarea-about"
aria-invalid={fieldState.invalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Select
- For select components, use `field.value` and `field.onChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-select"
className="sm:[&_.preview]:h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,22}
<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="responsive" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger
id="form-rhf-select-language"
aria-invalid={fieldState.invalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
/>
```
### Checkbox
- For checkbox arrays, use `field.value` and `field.onChange` with array manipulation.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-checkbox"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {10,15,20-22,38}
<Controller
name="tasks"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<Checkbox
id={`form-rhf-checkbox-${task.id}`}
name={field.name}
aria-invalid={fieldState.invalid}
checked={field.value.includes(task.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, task.id]
: field.value.filter((value) => value !== task.id)
field.onChange(newValue)
}}
/>
<FieldLabel
htmlFor={`form-rhf-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Radio Group
- For radio groups, use `field.value` and `field.onChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-radiogroup"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12-13,17,25,31}
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-rhf-radiogroup-${plan.id}`}
aria-invalid={fieldState.invalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Switch
- For switches, use `field.value` and `field.onChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-switch"
className="sm:[&_.preview]:h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,18-19}
<Controller
name="twoFactor"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Switch
id="form-rhf-switch-twoFactor"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-complex"
className="sm:[&_.preview]:h-[1300px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
React Hook Form provides a `useFieldArray` hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
<ComponentPreview
styleName="new-york-v4"
name="form-rhf-array"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
### Using useFieldArray
Use the `useFieldArray` hook to manage array fields. It provides `fields`, `append`, and `remove` methods.
```tsx showLineNumbers title="form.tsx" {8-11}
import { useFieldArray, useForm } from "react-hook-form"
export function ExampleForm() {
const form = useForm({
// ... form config
})
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "emails",
})
}
```
### Array Field Structure
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
```tsx showLineNumbers title="form.tsx"
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>
```
### Controller Pattern for Array Items
Map over the `fields` array and use `<Controller />` for each item. **Make sure to use `field.id` as the key**.
```tsx showLineNumbers title="form.tsx"
{
fields.map((field, index) => (
<Controller
key={field.id}
name={`emails.${index}.address`}
control={form.control}
render={({ field: controllerField, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
{...controllerField}
id={`form-rhf-array-email-${index}`}
aria-invalid={fieldState.invalid}
placeholder="name@example.com"
type="email"
autoComplete="email"
/>
{/* Remove button */}
</InputGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
/>
))
}
```
### Adding Items
Use the `append` method to add new items to the array.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use the `remove` method to remove items from the array. Add the remove button conditionally.
```tsx showLineNumbers title="form.tsx"
{
fields.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}
```
### Array Validation
Use Zod's `array` method to validate array fields.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```

View File

@@ -0,0 +1,707 @@
---
title: TanStack Form
description: Build forms in React using TanStack Form and Zod.
links:
doc: https://tanstack.com/form
---
import { InfoIcon } from "lucide-react"
This guide explores how to build forms using TanStack Form. You'll learn to create forms with the `<Field />` component, implement schema validation with Zod, handle errors, and ensure accessibility.
## Demo
We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in TanStack
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-demo"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses TanStack Form's `useForm` hook for form state management.
- `form.Field` component with render prop pattern for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod.
- Real-time validation feedback.
## Anatomy
Here's a basic example of a form using TanStack Form with the `<Field />` component.
```tsx showLineNumbers {15-31}
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
```
## Form
### Create a schema
We'll start by defining the shape of our form using a Zod schema.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation. TanStack Form
integrates seamlessly with Zod and other Standard Schema validation libraries
through its validators API.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Set up the form
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation.
```tsx showLineNumbers title="form.tsx" {10-21}
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("Form submitted successfully")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* ... */}
</form>
)
}
```
We are using `onSubmit` to validate the form data here. TanStack Form supports other validation modes, which you can read about in the [documentation](https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation).
### Build the form
We can now build the form using the `form.Field` component from TanStack Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-tanstack-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
## Validation
### Client-side Validation
TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
```tsx showLineNumbers title="form.tsx" {13-15}
import { useForm } from "@tanstack/react-form"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return <form onSubmit={/* ... */}>{/* ... */}</form>
}
```
### Validation Modes
TanStack Form supports different validation strategies through the `validators` option:
| Mode | Description |
| ------------ | ------------------------------------ |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit. |
```tsx showLineNumbers title="form.tsx" {6-9}
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {4,18}
<form.Field
name="email"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
## Working with Different Field Types
### Input
- For input fields, use `field.state.value` and `field.handleChange` on the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-input"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,11-14,22}
<form.Field
name="username"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="shadcn"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10 characters.
Must only contain letters, numbers, and underscores.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Textarea
- For textarea fields, use `field.state.value` and `field.handleChange` on the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-textarea"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,13-16,24}
<form.Field
name="about"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Select
- For select components, use `field.state.value` and `field.handleChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-select"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,18-19,23}
<form.Field
name="language"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)
}}
/>
```
### Checkbox
- For checkbox, use `field.state.value` and `field.handleChange` on the `<Checkbox />` component.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- For checkbox arrays, use `mode="array"` on the `<form.Field />` component and TanStack Form's array helpers.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-checkbox"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12,17,22-24,44}
<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Radio Group
- For radio groups, use `field.state.value` and `field.handleChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-radiogroup"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {21,29,35}
<form.Field
name="plan"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Switch
- For switches, use `field.state.value` and `field.handleChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-switch"
className="sm:[&_.preview]:h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,14,19-21}
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-complex"
className="sm:[&_.preview]:h-[1100px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
TanStack Form provides powerful array field management with `mode="array"`. This allows you to dynamically add, remove, and update array items with full validation support.
<ComponentPreview
styleName="new-york-v4"
name="form-tanstack-array"
className="sm:[&_.preview]:h-[700px]"
chromeLessOnMobile
/>
This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.
### Array Field Structure
Use `mode="array"` on the parent field to enable array field management.
```tsx showLineNumbers title="form.tsx" {3,12-14}
<form.Field
name="emails"
mode="array"
children={(field) => {
return (
<FieldSet>
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup>
{field.state.value.map((_, index) => (
// Nested field for each array item
))}
</FieldGroup>
</FieldSet>
)
}}
/>
```
### Nested Fields
Access individual array items using bracket notation: `fieldName[index].propertyName`. This example uses `InputGroup` to display the remove button inline with the input.
```tsx showLineNumbers title="form.tsx"
<form.Field
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched && !subField.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="name@example.com"
type="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError errors={subField.state.meta.errors} />
)}
</FieldContent>
</Field>
)
}}
/>
```
### Adding Items
Use `field.pushValue(item)` to add items to an array field. You can disable the button when the array reaches its maximum length.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use `field.removeValue(index)` to remove items from an array field. You can conditionally show the remove button only when there's more than one item.
```tsx showLineNumbers title="form.tsx"
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}
```
### Array Validation
Validate array fields using Zod's array methods.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```