Skip to content
GitHub

TanStack Form

Build forms in React using TanStack Form and Zod.

This guide shows how to use TanStack Form with Shark UI’s form primitives. Shark UI’s Field components are built on Ark UI, so they integrate cleanly with TanStack Form’s headless API while handling layout, error states, and accessibility.

Demo

Below is a bug report form: a text input for the title and a textarea for the description. Validation runs on submit and error messages appear next to each field.

Note: Browser validation is disabled in this demo to illustrate schema validation. In production, keep native validation enabled when appropriate.

Approach

TanStack Form provides headless form state; Shark UI’s Ark-based Field primitives take care of layout and semantics. Together they give full control over markup and styling.

  • Form state: TanStack Form’s useForm and form.Field with a render-prop pattern
  • Layout & semantics: Shark UI Field, FieldLabel, FieldError, and related components
  • Validation: Zod schema via the validators API, with configurable timing (onSubmit, onChange, onBlur)

Anatomy

Each field uses form.Field with a render function; Shark UI’s Field wraps the control for labels and errors.

Form

Create a schema

Define your form shape with a Zod schema.

Note: TanStack Form works with Zod and other Standard Schema libraries via its validators API.

Setup the form

Create the form with useForm, pass your schema into validators.onSubmit, and wire the submit handler.

Validation runs on submit in this example. You can also use onChange and onBlur validators—see the TanStack Form validation docs for details.

Done

The form is set up: Field components handle layout and a11y, and validation errors flow into field.state.meta.errors for FieldError to render.

Validation

Client-side validation

Pass your Zod schema into validators.onSubmit (or onChange / onBlur for different timing). Validation results are available in field.state.meta.

Validation modes

Configure when validation runs via the validators option:

Displaying errors

Use FieldError and pass invalid to both the Field wrapper and the control (Input, SelectTrigger, Checkbox, etc.) so styling and ARIA stay in sync.

Working with different field types

Input

Bind field.state.value and field.handleChange to Input; pass invalid to both Field and the input.

Textarea

Same pattern: bind field.state.value and field.handleChange to Textarea, and pass invalid to both Field and the textarea.

NativeSelect

Use NativeSelect for a native HTML <select>. Wire field.state.value and field.handleChange; pass invalid to the component.

Select

Use Select for a custom dropdown with a collection. Wire value and onValueChange; for single select, value is string[] with one item. For custom components like Select or Combobox, call field.handleBlur() from onInteractOutside so TanStack Form stays in sync when the user clicks away.

Checkbox

For single checkboxes, use field.state.value and field.handleChange. For checkbox lists, use mode="array" and TanStack Form’s pushValue / removeValue. Set data-slot="checkbox-group" on FieldGroup for spacing.

Radio group

Wire field.state.value and field.handleChange to RadioGroup; pass invalid to both Field and RadioGroupItem.

Switch

Use field.state.value and field.handleChange with Switch; pass invalid to both Field and the switch.

NumberField

Use field.state.value and field.handleChange with NumberField; the onValueChange callback provides { value }, which you pass to field.handleChange. Pass invalid to both Field and NumberField.

Slider

Use field.state.value (as number[]) and field.handleChange with Slider; the onValueChange callback provides { value }. Single-thumb sliders use [50], so field.state.value is the first element.

Segment Group

Use field.state.value and field.handleChange with SegmentGroup; pass invalid to Field. Wire value and onValueChange to the root.

Combobox

Use field.state.value and field.handleChange with Combobox; the onValueChange callback provides the selected value. For custom components like Combobox, call field.handleBlur() from onInteractOutside so TanStack Form stays in sync when the user clicks away.

Date Picker

Use field.state.value and field.handleChange with DatePicker; the value is a DateValue from @internationalized/date. Convert to/from ISO string for storage if needed.

Input OTP

Use field.state.value (as string[]) and field.handleChange with InputOtp; the onValueChange callback provides { value }. Join the array for submission (e.g. value.join("")).

Color Picker

Use field.state.value (hex string) and field.handleChange with ColorPicker; wire value and onValueChange. Use ColorPickerInput for an inline input, or ColorPickerTrigger + popover for a picker UI.

Rating

Use field.state.value (number) and field.handleChange with Rating; the onValueChange callback provides { value }.

File Upload

FileUpload uses a native hidden input, so it participates in form submission. Use form.Field with onFileAccept to sync the file list into form state.

Resetting the form

Call form.reset() to restore default values.

Array fields

Use mode="array" on form.Field when you need dynamic lists of fields (e.g. multiple emails). TanStack Form handles add/remove and validation for array items.

Array field structure

Set mode="array" on the parent field.

Nested fields

Access individual array items using bracket notation: fieldName[index].propertyName. This example uses InputGroup to display the remove button inline with the input.

Adding items

Call field.pushValue(newItem) to append a new entry.

Removing items

Call field.removeValue(index) to delete an entry. Usually you only show the remove action when there’s more than one item.

Array validation

Validate arrays with Zod’s z.array() and .min() / .max().