TanStack Form
Form integration with TanStack Form library.
When to Use
Use TanStack Form when you need to:
- Build type-safe forms with fine-grained field-level subscriptions for minimal re-renders
- Implement async validation with built-in debouncing (e.g., checking username availability)
- Share form logic across frameworks (React, Vue, Solid, Angular)
- Keep bundle size minimal while retaining full form capabilities (~5kb gzipped)
- Manage complex multi-step or conditional form workflows with headless control
When Not to Use
- Simple forms with 1-2 fields and no validation - use native HTML form handling or Field
- Projects already committed to React Hook Form - use React Hook Form with the shadcn/ui Form component
- Schema-first validation with Zod is the primary requirement - the Form component has tighter Zod integration
- Static forms with no client-side validation - use a server action with a plain
<form>
Basic Form
Simple form with a single input, synchronous validation, and submit state management.
Multi-field Form
Form with multiple fields arranged in a responsive grid layout with per-field validation.
With Select
Integrating a Select dropdown component with TanStack Form using onValueChange for controlled state.
With Checkboxes
Boolean fields using Checkbox components for toggleable options like terms acceptance.
Async Validation
Debounced async validation for checking server-side constraints. Try 'admin' or 'root' to see validation errors.
UX & Design Guidelines
Visual Hierarchy
Group related fields together with clear visual sections. Use space-y-6 between field groups and space-y-2 between label, input, and error message within a single field. Place the primary submit action at the bottom of the form and align it to the left for top-to-bottom reading flow.
Spacing & Layout
Use grid grid-cols-2 gap-4 for related short fields like first and last name. Keep forms at max-w-sm for single-column forms or max-w-lg for multi-column forms. Always constrain form width to prevent inputs from stretching too wide on desktop viewports.
Responsive Behavior
Multi-column grids should collapse to a single column on mobile. Use grid-cols-1 md:grid-cols-2 for responsive field layouts. Submit buttons should be full-width (w-full) on mobile for easier touch targets and inline on larger screens.
Color & Contrast
Error messages use text-destructive for immediate visual feedback. Description text uses text-muted-foreground to differentiate from primary content. The async validation "Checking..." indicator uses muted foreground to avoid alarming the user while a background check is in progress.
Accessibility
Keyboard Navigation
- Tab — Move focus between form fields in document order
- Shift + Tab — Move focus to the previous field
- Enter — Submit the form when a submit button has focus
- Space — Toggle checkbox fields and activate buttons
Screen Reader Support
- Every input must have an associated
Labelwith matchinghtmlForandidattributes - Error messages should use
role="alert"so screen readers announce them when they appear - Description text should be linked to the input via
aria-describedby - The async "Checking..." status should use
aria-live="polite"for non-intrusive announcements
Focus Management
Inputs display a visible focus ring using focus-visible:ring-[3px]. On validation failure after form submission, focus should be programmatically moved to the first invalid field so the user can correct it immediately. Use field.state.meta.errors to determine which field needs attention.
Error Announcements
Mark invalid inputs with aria-invalid="true" when errors are present. Link error messages to their inputs using aria-describedby pointing to the error element's id. This ensures assistive technology users can discover what went wrong without visual cues.
ARIA Attributes
{/* Associate labels with fields using htmlFor */}
<Label htmlFor={field.name}>Username</Label>
<Input
id={field.name}
name={field.name}
aria-invalid={field.state.meta.errors.length > 0}
aria-describedby={`${field.name}-error ${field.name}-description`}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{/* Error message with matching id */}
{field.state.meta.errors.length > 0 && (
<p id={`${field.name}-error`} role="alert" className="text-sm text-destructive">
{field.state.meta.errors[0]}
</p>
)}
{/* Description with matching id */}
<p id={`${field.name}-description`} className="text-sm text-muted-foreground">
Your unique display name.
</p>Related Components
API Reference
The useForm hook accepts the following options for configuring form behavior.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValuesrequired | TFormData | — | The initial values for the form fields. Infers TypeScript types for all field names. |
| onSubmitrequired | ({ value, formApi }) => void | Promise<void> | — | Callback fired when the form is submitted and all validation passes. |
| validators | FormValidators | {} | Form-level validators that run on the entire form state (onChange, onBlur, onSubmit). |
| validatorAdapter | ValidatorAdapter | undefined | Optional adapter for schema-based validation libraries like Zod or Valibot. |
| onSubmitInvalid | ({ value, formApi }) => void | undefined | Callback fired when submission is attempted but validation fails. |
| asyncDebounceMs | number | 0 | Default debounce time in milliseconds for all async validators on the form. |
Field Props
The form.Field component accepts the following props for individual field configuration.
| Prop | Type | Default | Description |
|---|---|---|---|
| namerequired | keyof TFormData | — | The field name. Must match a key from the form's defaultValues. |
| childrenrequired | (fieldApi: FieldApi) => ReactNode | — | Render function that receives the field API with state, handlers, and metadata. |
| validators | FieldValidators | {} | Field-level validators (onChange, onBlur, onChangeAsync, onBlurAsync) with optional debounce. |
| defaultValue | TFieldValue | undefined | Override the default value for this specific field. |
| asyncDebounceMs | number | 0 | Debounce time in milliseconds for async validators on this field. |