UI Library Integration
TanStack Form is headless — it manages state and validation but renders nothing. That means you wire it to whatever UI library your project already uses. The good news: the pattern is always the same.
The General Pattern
Most UI components accept three props: value, onChange, and onBlur. TanStack Form exposes exactly those things through the field object:
<form.Field
name="email"
children={(field) => (
<SomeUIInput
value={field.state.value}
onChange={(val) => field.handleChange(val)}
onBlur={field.handleBlur}
/>
)}
/>
field.state.value is the current value. field.handleChange updates it. field.handleBlur marks the field as touched. That's the whole adapter layer.
shadcn/ui Input
shadcn/ui components use standard HTML input props, so onChange gives you a DOM event:
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
<form.Field
name="email"
children={(field) => (
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</div>
)}
/>
The only difference from a plain <input> is that you're using the styled component. The wiring is identical.
Select / Combobox Components
Many select components skip the DOM event entirely and pass the selected value directly to a callback:
<form.Field
name="country"
children={(field) => (
<Select
value={field.state.value}
onValueChange={(val) => field.handleChange(val)}
>
{/* options */}
</Select>
)}
/>
Notice onValueChange instead of onChange. That's shadcn/ui's Select API — it hands you the value, not an event. Always check a component's API docs to see what the change callback receives.
Checkbox Components
Checkboxes deal in booleans, not strings:
<form.Field
name="agreeToTerms"
children={(field) => (
<Checkbox
checked={field.state.value}
onCheckedChange={(checked) => field.handleChange(Boolean(checked))}
/>
)}
/>
The Boolean() cast handles the fact that some checkbox components pass "indeterminate" in addition to true/false. Coercing it to a boolean keeps your form state clean.
The Key Insight
Adapting any UI component to TanStack Form takes two steps:
- How does the component expose its current value? Wire that to
field.state.value. - How does it notify you of changes? Wire that callback to
field.handleChange.
That's it. The adapter is always thin. If a component has an unusual API, read its docs, find those two things, and map them. Everything else — validation, error display, touched state — works the same regardless of which component library you're using.
Complete Example
Here's a full form using plain HTML elements that demonstrates all three patterns — text input, select, and checkbox:
import { useForm } from '@tanstack/react-form'
function SettingsForm() {
const form = useForm({
defaultValues: {
displayName: '',
theme: 'light',
emailNotifications: false,
},
onSubmit: async ({ value }) => {
console.log('Settings saved:', value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="displayName"
children={(field) => (
<div>
<label htmlFor="displayName">Display Name</label>
<input
id="displayName"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</div>
)}
/>
<form.Field
name="theme"
children={(field) => (
<div>
<label htmlFor="theme">Theme</label>
<select
id="theme"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
</div>
)}
/>
<form.Field
name="emailNotifications"
children={(field) => (
<div>
<label>
<input
type="checkbox"
checked={field.state.value}
onChange={(e) => field.handleChange(e.target.checked)}
/>
Email notifications
</label>
</div>
)}
/>
<button type="submit">Save Settings</button>
</form>
)
}
The same patterns apply when you swap <input> for a shadcn/ui <Input>, <select> for a Radix <Select>, or the checkbox for any third-party toggle. The wiring stays the same.