docs/forms/tanstack-form
Sections
Components
Utilities
Forms
Hooks
Copy Markdown
Build forms in React using TanStack Form and Zod.
This guide will cover building forms using the Field component, adding schema validation with Zod, handling errors, ensuring accessibility, and more.
We’ll build a form with a text input and textarea. When you submit, the form data is validated and any errors will be shown.
Browser validation disabled
For the purposes of this demo, browser validation is disabled to illustrate schema validation. In production, keep native validation enabled when appropriate.
PreviewCode
Bug Report
Help us improve by reporting bugs you encounter.
Bug Title
Description
0/100 characters
Include steps to reproduce, expected behavior, and what actually happened.
ResetSubmit
This form uses TanStack Form for state and Zod for validation. We'll build forms using the Field component, which gives you complete flexibility over the markup and styling.
useForm hook for form state management.form.Field with a render function for controlled inputs.Field components for building accessible forms.validators.Here's a basic example of a form using TanStack Form with the Field component.
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => (
<Field invalid={!field.state.meta.isValid}>
<FieldLabel>Bug Title</FieldLabel>
<Input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError>
{field.state.meta.errors
.map((e) => e?.message || e)
.join(", ")}
</FieldError>
</Field>
)}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
Define your form shape with a Zod schema.
Note: TanStack Form works with Zod and other Standard Schema libraries via its validators API.
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."),
})
useForm from TanStack Form and pass your schema to the onSubmit option.form.tsx
import { useForm } from "@tanstack/react-form";
import * as z from "zod";
const formSchema = z.object({
// ...
});
export const ExampleForm = () => {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
console.log(value);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation()
form.handleSubmit();
}}
>
</form>
);
};
build the form using the form.Field from TanStack Form and the Shark Field.
Expand
"use client";
import { toast } from "@registry/react/components/toast";
import { useForm } from "@tanstack/react-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group";
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 const BugReportForm = () => {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
toast.info({
id: "bug-report-submitted",
title: "Bug submitted",
description: (
<pre className="mt-2">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
});
},
});
return (
<Card asChild className="w-full sm:max-w-md">
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<CardHeader
description="Help us improve by reporting bugs you encounter."
title="Bug Report"
/>
<CardContent>
<FieldGroup>
<form.Field
children={(field) => (
<Field invalid={!field.state.meta.isValid}>
<FieldLabel>Bug Title</FieldLabel>
<Input
autoComplete="off"
name={field.name}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Login button not working on mobile"
value={field.state.value}
/>
<FieldError>
{field.state.meta.errors.map((e) => e?.message).join(", ")}
</FieldError>
</Field>
)}
name="title"
/>
<form.Field
children={(field) => (
<Field invalid={!field.state.meta.isValid}>
<FieldLabel>Description</FieldLabel>
<InputGroup>
<InputGroupTextarea
className="min-h-24 resize-none"
name={field.name}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="I'm having an issue with the login button on mobile."
rows={6}
value={field.state.value}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.state.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
<FieldError>
{field.state.meta.errors.map((e) => e?.message).join(", ")}
</FieldError>
</Field>
)}
name="description"
/>
</FieldGroup>
</CardContent>
<CardFooter>
<Button onClick={() => form.reset()} variant="outline">
Reset
</Button>
<Button type="submit">Submit</Button>
</CardFooter>
</form>
</Card>
);
};
Expand
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the onSubmit handler receives validated values. If the form is invalid, TanStack Form exposes errors on field.state.meta.errors for FieldError.
TanStack Form validates your form data using the Zod schema. Define a schema and pass it to the validators option of the useForm hook.
example-form.tsx
import { useForm } from "@tanstack/react-form"
import * as z from "zod"
const formSchema = z.object({
title: z.string(),
description: z.string().optional(),
})
export const ExampleForm = () => {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: { onSubmit: formSchema },
onSubmit: async () => {},
})
}
Configure when validation runs via the validators option:
form.tsx
const form = useForm({
validators: { onSubmit: formSchema },
})
| Mode | Description |
|---|---|
"onChange" | Validation triggers on every change. |
"onBlur" | Validation triggers on blur. |
"onSubmit" | Validation triggers on submit. |
Display errors next to the field using FieldError. For styling and accessibility:
invalid prop to the Field component.invalid prop to the form control such as Input, Checkbox, etc.form.tsx
<form.Field
name="email"
children={(field) => (
<Field invalid={!field.state.meta.isValid}>
<FieldLabel>Email</FieldLabel>
<Input
name={field.name}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
value={field.state.value}
/>
<FieldError>
{field.state.meta.errors.map((e) => e?.message).join(", ")}
</FieldError>
</Field>
)}
/>
field.state.value and field.handleChange to Input.invalid prop to the Field component and pass the error message to the FieldError component.PreviewCode
Profile Settings
Update your profile information below.
Username
This is your public display name. Must be between 3 and 10 characters. Must only contain letters, numbers, and underscores.
ResetSave
field.state.value and field.handleChange to Textarea.invalid prop to the Field component and pass the error message to the FieldError component.PreviewCode
Personalization
Customize your experience by telling us more about yourself.
More about you
Tell us more about yourself. This will be used to help us personalize your experience.
ResetSave
field.state.value and field.handleChange to NativeSelect.invalid prop to the Field component and pass the error message to the FieldError component.PreviewCode
Language Preferences
Select your preferred spoken language.
Spoken Language
For best results, select the language you speak.
AutoEnglishSpanishFrenchGermanItalianChineseJapanese
ResetSave
value and onValueChange on Select. For overlays, call field.handleBlur() from onInteractOutside when your example needs blur sync.invalid prop to the Field component and pass the error message to the FieldError component.PreviewCode
Language Preferences
Select your preferred spoken language.
Spoken Language
For best results, select the language you speak.
Select
AutoEnglishSpanishFrenchGermanItalianChineseJapanese
ResetSave
field.state.value and field.handleChange to Checkbox.invalid prop to the Field component and pass the error message to the FieldError component.mode="array" on the form.Field component and TanStack Form's array helpers.data-slot="checkbox-group" to the FieldGroup component for proper styling and spacing.PreviewCode
Notifications
Manage your notification preferences.
Responses
Get notified for requests that take time, like research or image generation.
Push notifications
Tasks
Get notified when tasks you've created have updates.
Push notifications
Email notifications
ResetSave
field.state.value and field.handleChange to RadioGroup.invalid prop to the Field component and pass the FieldError component.PreviewCode
Subscription Plan
See pricing and features for each plan.
Plan
You can upgrade or downgrade your plan at any time.
Starter (100K tokens/month)
For everyday use with basic features.
Pro (1M tokens/month)
For advanced AI usage with more features.
Enterprise (Unlimited tokens)
For large teams and heavy usage.
ResetSave
field.state.value and field.handleChange with Switch.invalid prop to the Field component and pass the FieldError component.PreviewCode
Security Settings
Manage your account security preferences.
Multi-factor authentication
Enable multi-factor authentication to secure your account.
ResetSave
field.state.value and field.handleChange to NumberInput.invalid prop to the Field component and pass the FieldError component.PreviewCode
Salary expectations
Share your target gross annual salary in EUR.
Expected annual salary
€
EUR
ResetSave
field.state.value and field.handleChange to Slider.invalid prop to the Field component and pass the FieldError component.PreviewCode
Listings filter
Set a price range so we only show results in your budget.
Price range$0 - $10000
Drag each thumb to set the lower and upper bound.
ResetApply
field.state.value and field.handleChange to Combobox.invalid prop to the Field component and pass the FieldError component.PreviewCode
Role setup
Tell us which team you work with most so we can tailor workflows.
Primary department
Type to filter the list, then pick one option.
ResetSave
field.state.value and field.handleChange to Autocomplete.invalid prop to the Field component and pass the FieldError component.PreviewCode
Tech stack
Select the technology you're most familiar with.
Primary technology
Type to filter the list, then pick one option.
ResetSave
field.state.value and field.handleChange to DatePicker.invalid prop to the Field component and pass the FieldError component.PreviewCode
Scheduling
Pick a day that works for a first-round interview. We will confirm by email.
Preferred interview date
Pick a date
ResetSave
field.state.value (as string[]) and field.handleChange with InputOTP.invalid prop to the Field component and pass the FieldError component.PreviewCode
Account recovery
Enter one of your single-use backup codes if you cannot access your authenticator app.
Backup code
Codes can only be used once.
ResetSave
field.state.value and field.handleChange to Rating.invalid prop to the Field component and pass the FieldError component.PreviewCode
Quick feedback
Help us improve by sharing how likely you are to recommend Shark UI to a colleague.
How useful is this project?
1 = not likely, 5 = very likely.
ResetSave
onFileAccept to sync files into form state.invalid prop to the Field component and pass the FieldError component.PreviewCode
Application materials
Upload your résumé or CV.
Résumé
Accepted formats: PDF and Word documents.
ResetSave
Here is an example of a more complex form with multiple fields and validation.
PreviewCode
You're almost there!
Choose your subscription plan and billing period.
Subscription plan
Choose your subscription plan.
Basic
For individuals and small teams
Pro
For businesses with higher demands
Billing period
Select
MonthlyYearly
Choose how often you want to be billed.
Add-ons
Select additional features you'd like to include.
Analytics
Advanced analytics and reporting
Backup
Automated daily backups
Priority Support
24/7 premium customer support
Email notifications
Receive email updates about your subscription.
Save preferencesReset
Use form.reset() to reset the form to its default values.
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
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.
PreviewCode
Contact emails
Manage your contact email addresses.
Email addresses
Add up to 5 email addresses where we can contact you.
Add email address
ResetSave
Use mode="array" on the parent field to enable array field management.
form.tsx
<form.Field
name="emails"
mode="array"
children={(field) => (
<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>
)}
/>
Access individual array items using bracket notation: fieldName[index].propertyName.
form.tsx
<form.Field
name={`emails[${index}].address`}
children={(subField) => (
<Field orientation="horizontal" invalid={!subField.state.meta.isValid}>
<FieldContent>
<InputGroup>
<InputGroupInput
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
placeholder="[email protected]"
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>
<FieldError>
{subField.state.meta.errors.map((e) => e?.message).join(", ")}
</FieldError>
</FieldContent>
</Field>
)}
/>
Use field.pushValue(item) to add items to an array field. You can disable the button when the array reaches its maximum length.
form.tsx
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>
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.
form.tsx
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}
Use removeValue(index) on the array field.
form.tsx
{
emailsField.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => emailsField.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
);
}
Validate arrays with Zod’s z.array() and .min() / .max().
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."),
});
[
Previous page
React Hook Form ](/docs/forms/react-hook-form)[
Next page
useIsMobile ](/docs/hooks/use-is-mobile)
On This Page
DemoApproachAnatomyFormCreate a schemaSetupBuildDoneValidationClient-sideModesDisplaying ErrorsDifferent types of fieldsInputTextareaNativeSelectSelectCheckboxRadio groupSwitchNumberInputSliderComboboxAutocompleteDate PickerInput OTPRatingFile UploadComplex FormsResetting the FormArray FieldsArray Field StructureNested FieldsAdding ItemsRemoving ItemsRemoving itemsArray validation