TanStack Form is a headless library, offering you complete flexibility to style it as you see fit. It’s compatible with a wide range of UI libraries, including Chakra UI, Tailwind, Material UI, Mantine, shadcn/ui, or even plain CSS.
This guide demonstrates integration with popular UI libraries, but the concepts are applicable to any UI library of your choice.
Installation
Before integrating TanStack Form with a UI library, ensure the necessary dependencies are installed:
While you can mix and match libraries, it’s generally advisable to stick with one to maintain consistency and minimize bloat.
Mantine Integration
Here’s an example demonstrating the integration of TanStack Form with Mantine components:
import { TextInput, Checkbox } from '@mantine/core'
import { useForm } from '@tanstack/react-form'
export default function App() {
const form = useForm({
defaultValues: {
name: '',
isChecked: false,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="name"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Name must be at least 3 characters' : undefined,
}}
>
{({ state, handleChange, handleBlur }) => (
<TextInput
label="Name"
placeholder="Enter your name"
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
error={state.meta.errors[0]}
/>
)}
</form.Field>
<form.Field name="isChecked">
{({ state, handleChange, handleBlur }) => (
<Checkbox
label="Accept terms"
checked={state.value}
onChange={(e) => handleChange(e.target.checked)}
onBlur={handleBlur}
/>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
Key Integration Points
- The
Field component uses render props to integrate with Mantine components
- We destructure
state, handleChange, and handleBlur from the field
- We pass these props to the Mantine components, adapting to their specific APIs
- Error messages are displayed using Mantine’s built-in error prop
TanStack Form’s design relies heavily on render props, providing type-safe access to field state and handlers without unnecessary abstractions.
Material UI Integration
The integration process is similar for Material UI components:
import TextField from '@mui/material/TextField'
import { Checkbox } from '@mui/material'
import { useForm } from '@tanstack/react-form'
export default function App() {
const form = useForm({
defaultValues: {
lastName: '',
isMuiCheckBox: false,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field name="lastName">
{({ state, handleChange, handleBlur }) => (
<TextField
label="Last Name"
variant="filled"
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
placeholder="Enter your last name"
error={state.meta.errors.length > 0}
helperText={state.meta.errors[0]}
/>
)}
</form.Field>
<form.Field name="isMuiCheckBox">
{({ state, handleChange, handleBlur }) => (
<Checkbox
checked={state.value}
onChange={(e) => handleChange(e.target.checked)}
onBlur={handleBlur}
/>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
Material UI Specifics
- Use the
error prop (boolean) to indicate validation errors
- Use the
helperText prop to display error messages
- The
variant prop controls the visual style (outlined, filled, standard)
shadcn/ui Integration
The process for integrating shadcn/ui components:
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import { useForm } from '@tanstack/react-form'
export default function App() {
const form = useForm({
defaultValues: {
name: '',
isChecked: false,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field name="name">
{({ state, handleChange, handleBlur }) => (
<div className="space-y-2">
<Input
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
placeholder="Enter your name"
/>
{state.meta.errors[0] && (
<p className="text-sm text-destructive">
{state.meta.errors[0]}
</p>
)}
</div>
)}
</form.Field>
<form.Field name="isChecked">
{({ state, handleChange, handleBlur }) => (
<Checkbox
checked={state.value}
onCheckedChange={(checked) => handleChange(checked === true)}
onBlur={handleBlur}
/>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
shadcn/ui Specifics
- Note the
onCheckedChange prop instead of onChange for Checkbox
- Error messages are styled with Tailwind utility classes
- Components are typically imported from your local
components/ui directory
Chakra UI Integration
Chakra UI integration with both composable and closed components:
import { Input } from '@chakra-ui/react'
import { Checkbox } from '@chakra-ui/react'
import { useForm } from '@tanstack/react-form'
export default function App() {
const form = useForm({
defaultValues: {
name: '',
isChecked: false,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field name="name">
{({ state, handleChange, handleBlur }) => (
<Input
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
placeholder="Enter your name"
/>
)}
</form.Field>
<form.Field name="isChecked">
{({ state, handleChange, handleBlur }) => (
<Checkbox
checked={state.value}
onCheckedChange={(details) => handleChange(!!details.checked)}
onBlur={handleBlur}
>
Accept terms
</Checkbox>
)}
</form.Field>
</form>
)
}
Chakra UI Composable Components
Chakra UI v3 exposes many components as composable parts:
import { Checkbox } from '@chakra-ui/react'
<form.Field name="isChecked">
{({ state, handleChange, handleBlur }) => (
<Checkbox.Root
checked={state.value}
onCheckedChange={(details) => handleChange(!!details.checked)}
onBlur={handleBlur}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
<Checkbox.Label>Accept terms</Checkbox.Label>
</Checkbox.Root>
)}
</form.Field>
Common Patterns
Error Display
Most UI libraries have their own error display patterns:
// Mantine
<TextInput error={state.meta.errors[0]} />
// Material UI
<TextField
error={state.meta.errors.length > 0}
helperText={state.meta.errors[0]}
/>
// shadcn/ui
{state.meta.errors[0] && (
<p className="text-sm text-destructive">{state.meta.errors[0]}</p>
)}
Validation on Blur
All integrations follow the same pattern for blur validation:
<form.Field
name="email"
validators={{
onBlur: ({ value }) =>
!value.includes('@') ? 'Invalid email' : undefined,
}}
>
{({ state, handleChange, handleBlur }) => (
<Input
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur} // Important!
/>
)}
</form.Field>
Async Validation
Async validation works the same across all UI libraries:
<form.Field
name="username"
validators={{
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
const exists = await checkUsername(value)
return exists ? 'Username taken' : undefined
},
}}
>
{({ state, handleChange, handleBlur }) => (
<div>
<Input
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
{state.meta.isValidating && <span>Checking...</span>}
</div>
)}
</form.Field>
Best Practices
- Use render props: They provide type safety and flexibility
- Handle all events: Pass
handleChange, handleBlur, and other handlers to maintain form state
- Adapt to component APIs: Different UI libraries have different prop names (e.g.,
onChange vs onCheckedChange)
- Display errors appropriately: Use each library’s preferred error display mechanism
- Type safety: Let TypeScript infer types from your form definition
Start with the basic examples in this guide and adapt them to your specific UI library’s component API and styling patterns.