.agents/skills/tanstack-form/SKILL.md
Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
npx skillsauth add iEnergyy/opsflow tanstack-formInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).
Package: @tanstack/react-form
Adapters: @tanstack/zod-form-adapter, @tanstack/valibot-form-adapter
Status: Stable (v1)
npm install @tanstack/react-form
# Optional schema adapters:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot
import { useForm } from '@tanstack/react-form'
function MyForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
// value is fully typed
await submitToServer(value)
},
onSubmitInvalid: ({ value, formApi }) => {
console.log('Validation failed:', formApi.state.errors)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* Fields */}
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
children={({ canSubmit, isSubmitting }) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
/>
</form>
)
}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Must be at least 3 characters' : undefined,
}}
children={(field) => (
<div>
<label htmlFor={field.name}>First Name</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
<em>{field.state.meta.errors.join(', ')}</em>
)}
</div>
)}
/>
<!-- Nested fields use dot notation -->
<form.Field name="address.city">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
| Cause | When |
|-------|------|
| onChange | After every value change |
| onBlur | When field loses focus |
| onSubmit | During submission |
| onMount | When field mounts |
<form.Field
name="age"
validators={{
onChange: ({ value }) => {
if (value < 18) return 'Must be 18 or older'
return undefined // undefined = valid
},
onBlur: ({ value }) => {
if (!value) return 'Required'
return undefined
},
}}
/>
<form.Field
name="username"
asyncDebounceMs={500}
validators={{
onChangeAsync: async ({ value }) => {
const res = await fetch(`/api/check-username?q=${value}`)
const { available } = await res.json()
if (!available) return 'Username taken'
return undefined
},
}}
>
{(field) => (
<>
<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
{field.state.meta.isValidating && <span>Checking...</span>}
</>
)}
</form.Field>
import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'
const form = useForm({
defaultValues: { email: '', age: 0 },
validatorAdapter: zodValidator(),
onSubmit: async ({ value }) => { /* ... */ },
})
<form.Field
name="email"
validators={{
onChange: z.string().email('Invalid email'),
onBlur: z.string().min(1, 'Required'),
}}
/>
<form.Field
name="age"
validators={{
onChange: z.number().min(18, 'Must be 18+'),
}}
/>
const form = useForm({
defaultValues: { password: '', confirmPassword: '' },
validators: {
onChange: ({ value }) => {
if (value.password !== value.confirmPassword) {
return 'Passwords do not match'
}
return undefined
},
},
})
<form.Field
name="confirmPassword"
validators={{
onChangeListenTo: ['password'], // Re-validate when password changes
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password')
if (value !== password) return 'Passwords do not match'
return undefined
},
}}
/>
<form.Field name="people" mode="array">
{(field) => (
<div>
{field.state.value.map((_, index) => (
<div key={index}>
<form.Field name={`people[${index}].name`}>
{(subField) => (
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
/>
)}
</form.Field>
<button type="button" onClick={() => field.removeValue(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
Add Person
</button>
</div>
)}
</form.Field>
field.pushValue(item) // Add to end
field.insertValue(index, item) // Insert at index
field.replaceValue(index, item) // Replace at index
field.removeValue(index) // Remove at index
field.swapValues(indexA, indexB) // Swap positions
field.moveValue(from, to) // Move position
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
// Side effect: reset dependent fields
form.setFieldValue('state', '')
form.setFieldValue('postalCode', '')
},
}}
/>
// Render-prop subscription (fine-grained)
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
children={({ canSubmit, isDirty }) => (
<div>
{isDirty && <span>Unsaved changes</span>}
<button disabled={!canSubmit}>Save</button>
</div>
)}
/>
// Hook-based subscription
function FormStatus() {
const isValid = form.useStore((s) => s.isValid)
return isValid ? null : <p>Fix errors</p>
}
interface FormState {
values: TFormData
errors: ValidationError[]
errorMap: Record<string, ValidationError>
isFormValid: boolean
isFieldsValid: boolean
isValid: boolean // isFormValid && isFieldsValid
isTouched: boolean
isPristine: boolean
isDirty: boolean
isSubmitting: boolean
isSubmitted: boolean
isSubmitSuccessful: boolean
submissionAttempts: number
canSubmit: boolean // isValid && !isSubmitting
}
interface FieldState<TData> {
value: TData
meta: {
isTouched: boolean
isDirty: boolean
isPristine: boolean
isValidating: boolean
errors: ValidationError[]
errorMap: Record<ValidationCause, ValidationError>
}
}
form.handleSubmit()
form.reset()
form.getFieldValue(field)
form.setFieldValue(field, value)
form.getFieldMeta(field)
form.setFieldMeta(field, updater)
form.validateAllFields(cause)
form.validateField(field, cause)
form.deleteField(field)
import { formOptions } from '@tanstack/react-form'
const sharedOpts = formOptions({
defaultValues: { firstName: '', lastName: '' },
})
// Reuse across components
const form = useForm({
...sharedOpts,
onSubmit: async ({ value }) => { /* ... */ },
})
// TanStack Start / Next.js server action
import { ServerValidateError } from '@tanstack/react-form/nextjs'
export async function validateForm(data: FormData) {
const email = data.get('email') as string
if (await checkEmailExists(email)) {
throw new ServerValidateError({
form: 'Submission failed',
fields: { email: 'Email already registered' },
})
}
}
// Type-safe field paths with DeepKeys
interface UserForm {
name: string
address: { street: string; city: string }
tags: string[]
contacts: Array<{ name: string; phone: string }>
}
// TypeScript auto-completes all valid paths:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" /> // OK
<form.Field name="nonexistent" /> // Type Error!
e.preventDefault() and e.stopPropagation() on form submitonBlur={field.handleBlur} for blur validation and isTouched trackingmode="array" for array fields to get array methodsundefined (not null/false) for valid validatorsasyncDebounceMs for async validators to prevent API spamisTouched before showing errors for better UXform.Subscribe with selectors to minimize re-rendersformOptions for shared configuration across componentsonChangeListenTo for cross-field validation dependenciese.preventDefault() on form submit (causes page reload)onBlur to inputs (breaks blur validation and isTouched)null or false instead of undefined for valid fieldsmode="array" incorrectly (only needed on the array field itself, not sub-fields)asyncDebounceMs with async validators (fires on every keystroke)development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
tools
UI/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, and HTML/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn/ui MCP for component search and examples.
development
Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular.
development
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.