- Accordion
- Alert
- Alert Dialog
- Badge
- Breadcrumbs
- Button Group
- Button
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- File Field
- Hover Card
- Kbd
- Menubar
- Navigation Menu
- Number Field
- OTP Field
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Search
- Segmented Control
- Select
- Separator
- Sidebar
- Skeleton
- Slider
- Sonner
- Switch
- Table
- Tabs
- Text Field
- Toggle Group
- Toggle Button
- Tooltip
This guide explores how to build forms using TanStack Form. You'll learn to create forms with the <TextField /> component, implement schema validation with Valibot, 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.
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.
Component tanstack-form-demo not found in registry.
Approach
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the <TextField /> component, which gives you complete flexibility over the markup and styling.
- Uses TanStack Form's
createFormhook for form state management. form.Fieldcomponent with render prop pattern for controlled inputs.<Field />components for building accessible forms.- Client-side validation using Valibot.
- Real-time validation feedback.
Anatomy
Here's a basic example of a form using TanStack Form with the <TextField /> component.
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field name="title">
{(field) => (
<TextField
validationState={
field().state.meta.isTouched && !field().state.meta.isValid
? "invalid"
: "valid"
}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onChange={field().handleChange}
>
<TextFieldLabel>Bug Title</TextFieldLabel>
<TextFieldInput
placeholder="Login button not working on mobile"
autocomplete="off"
/>
<TextFieldDescription>
Provide a concise title for your bug report.
</TextFieldDescription>
<TextFieldErrorMessage errors={field().state.meta.errors} />
</TextField>
)}
</form.Field>
<button type="submit">Login</button>
</form>Form
Create a schema
We'll start by defining the shape of our form using a Valibot schema.
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."),
),
})Setup the form
Use the createForm hook from TanStack Form to create your form instance with Valibot validation.
import { createForm } from "@tanstack/solid-form"
import { toast } from "somoto"
import * as v from "valibot"
const formSchema = v.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.
Build the form
We can now build the form using the form.Field component from TanStack Form and the <TextField /> component.
import { createForm } from "@tanstack/solid-form"
import { toast } from "somoto"
import * as v from "valibot"
import { Button } from "@/registry/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/ui/card"
import {
TextField,
TextFieldDescription,
TextFieldErrorMessage,
TextFieldInput,
TextFieldLabel,
TextFieldTextArea,
} from "@/registry/ui/text-field"
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."),
),
})
type formSchemaType = v.InferInput<typeof formSchema>
const TanstackFormDemo = () => {
const form = createForm(() => ({
defaultValues: {
title: "",
description: "",
} as formSchemaType,
validators: {
onSubmit: formSchema,
},
onSubmit: (props) => {
toast("You submitted the following values:", {
description: (
<pre class="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(props.value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
},
})
},
}))
return (
<Card class="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="bug-report-form"
onSubmit={(e) => {
e.preventDefault()
void form.handleSubmit()
}}
class="flex w-full flex-col gap-7"
>
<form.Field name="title">
{(field) => (
<TextField
validationState={
field().state.meta.isTouched && !field().state.meta.isValid
? "invalid"
: "valid"
}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onChange={field().handleChange}
>
<TextFieldLabel>Bug Title</TextFieldLabel>
<TextFieldInput
placeholder="Login button not working on mobile"
autocomplete="off"
/>
<TextFieldErrorMessage errors={field().state.meta.errors} />
</TextField>
)}
</form.Field>
<form.Field name="description">
{(field) => (
<TextField
validationState={
field().state.meta.isTouched && !field().state.meta.isValid
? "invalid"
: "valid"
}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onChange={field().handleChange}
>
<TextFieldLabel>Description</TextFieldLabel>
<TextFieldTextArea
placeholder="I'm having an issue with the login button on mobile."
rows={6}
class="min-h-24 resize-none"
/>
<TextFieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</TextFieldDescription>
<TextFieldErrorMessage errors={field().state.meta.errors} />
</TextField>
)}
</form.Field>
</form>
</CardContent>
<CardFooter>
<div class="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
form.reset()
}}
>
Reset
</Button>
<Button type="submit" form="bug-report-form">
Submit
</Button>
</div>
</CardFooter>
</Card>
)
}
export default TanstackFormDemoDone
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.
Working with Different Field Types
Input
- For input fields, use
field().state.valueandfield().handleChangeon the<TextField />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<FieldField />component.
Component tanstack-form-input-demo not found in registry.
Text Area
- For text area fields, use
field().state.valueandfield().handleChangeon the<TextField />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<FieldField />component.
Component tanstack-form-textarea-demo not found in registry.
Select
- For select component, use
field().state.valueandfield().handleChangeon the<Select />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<Select />component.
Component tanstack-form-select-demo not found in registry.
Checkbox
- For checkbox component, use
field().state.valueandfield().handleChangeon the<Checkbox />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<Checkbox />component.
Component tanstack-form-checkbox-demo not found in registry.
Radio Group
- For radio group component, use
field().state.valueandfield().handleChangeon the<RadioGroup />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<RadioGroup />component.
Component tanstack-form-radio-group-demo not found in registry.
Switch
- For switch component, use
field().state.valueandfield().handleChangeon the<Switch />component. - To show errors, pass
field().state.meta.errorsto thevalidationStateprop of the<Switch />component.
Component tanstack-form-switch-demo not found in registry.
Resetting the Form
Use form.reset() to reset the form to its default values.
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>