Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tanstack/form/llms.txt

Use this file to discover all available pages before exploring further.

TanStack Form provides highly customizable validation that gives you complete control over when and how validation runs.

Validation Control

You control:
  • When validation runs (onChange, onBlur, onSubmit, etc.)
  • Where validation is defined (field-level or form-level)
  • How validation executes (synchronous or asynchronous)

When Validation Runs

The form.Field component accepts validator callbacks that determine when validation occurs. Return an error message as a string to indicate validation failure, or undefined for success.

onChange Validation

Validate on every keystroke:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
  }}
>
  {(field) => (
    <>
      <label for={field().name}>Age:</label>
      <input
        id={field().name}
        name={field().name}
        value={field().state.value}
        type="number"
        onInput={(e) => field().handleChange(e.target.valueAsNumber)}
      />
      {!field().state.meta.isValid ? (
        <em role="alert">{field().state.meta.errors.join(', ')}</em>
      ) : null}
    </>
  )}
</form.Field>

onBlur Validation

Validate when the field loses focus:
<form.Field
  name="age"
  validators={{
    onBlur: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
  }}
>
  {(field) => (
    <>
      <label for={field().name}>Age:</label>
      <input
        id={field().name}
        name={field().name}
        value={field().state.value}
        type="number"
        onBlur={field().handleBlur}
        onInput={(e) => field().handleChange(e.target.valueAsNumber)}
      />
      {!field().state.meta.isValid ? (
        <em role="alert">{field().state.meta.errors.join(', ')}</em>
      ) : null}
    </>
  )}
</form.Field>

Multiple Validators

Run different validations at different times:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
    onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined),
  }}
>
  {(field) => (
    <>
      <label for={field().name}>Age:</label>
      <input
        id={field().name}
        name={field().name}
        value={field().state.value}
        type="number"
        onBlur={field().handleBlur}
        onInput={(e) => field().handleChange(e.target.valueAsNumber)}
      />
      {!field().state.meta.isValid ? (
        <em role="alert">{field().state.meta.errors.join(', ')}</em>
      ) : null}
    </>
  )}
</form.Field>

Displaying Errors

Using the errors Array

Map all errors for a field:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
  }}
>
  {(field) => (
    <>
      {/* ... */}
      {!field().state.meta.isValid ? (
        <em>{field().state.meta.errors.join(',')}</em>
      ) : null}
    </>
  )}
</form.Field>

Using errorMap

Access specific errors by validation type:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
  }}
>
  {(field) => (
    <>
      {/* ... */}
      {field().state.meta.errorMap['onChange'] ? (
        <em>{field().state.meta.errorMap['onChange']}</em>
      ) : null}
    </>
  )}
</form.Field>

Type-Safe Error Objects

Errors can be any type, not just strings:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
  }}
>
  {(field) => (
    <>
      {/* errorMap.onChange is type `{isOldEnough: false} | undefined` */}
      {!field().state.meta.errorMap['onChange']?.isOldEnough ? (
        <em>The user is not old enough</em>
      ) : null}
    </>
  )}
</form.Field>

Field-Level vs Form-Level Validation

Field-Level Validation

Validate individual fields:
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13 ? 'You must be 13 to make an account' : undefined,
  }}
>
  {(field) => (
    <input
      value={field().state.value}
      onInput={(e) => field().handleChange(e.target.valueAsNumber)}
    />
  )}
</form.Field>

Form-Level Validation

Validate the entire form at once:
export default function App() {
  const form = createForm(() => ({
    defaultValues: {
      age: 0,
    },
    onSubmit: async ({ value }) => {
      console.log(value)
    },
    validators: {
      onChange({ value }) {
        if (value.age < 13) {
          return 'Must be 13 or older to sign'
        }
        return undefined
      },
    },
  }))

  const formErrorMap = form.useStore((state) => state.errorMap)

  return (
    <div>
      {formErrorMap().onChange ? (
        <div>
          <em>There was an error on the form: {formErrorMap().onChange}</em>
        </div>
      ) : null}
    </div>
  )
}

Setting Field Errors from Form Validators

You can set field-level errors from form-level validators:
import { Show } from 'solid-js'
import { createForm } from '@tanstack/solid-form'

export default function App() {
  const form = createForm(() => ({
    defaultValues: {
      age: 0,
      socials: [],
      details: {
        email: '',
      },
    },
    validators: {
      onSubmitAsync: async ({ value }) => {
        const hasErrors = await validateDataOnServer(value)
        if (hasErrors) {
          return {
            form: 'Invalid data',
            fields: {
              age: 'Must be 13 or older to sign',
              'socials[0].url': 'The provided URL does not exist',
              'details.email': 'An email is required',
            },
          }
        }
        return null
      },
    },
  }))

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        void form.handleSubmit()
      }}
    >
      <form.Field
        name="age"
        children={(field) => (
          <>
            <input
              value={field().state.value}
              type="number"
              onChange={(e) => field().handleChange(e.target.valueAsNumber)}
            />
            <Show when={field().state.meta.errors.length > 0}>
              <em role="alert">{field().state.meta.errors.join(', ')}</em>
            </Show>
          </>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  )
}
Field-specific validation will overwrite form-level validation for the same field. If both form and field validators return errors for the same field, only the field-level error will be shown.

Asynchronous Validation

Use async validators for network calls or other async operations:
<form.Field
  name="age"
  validators={{
    onChangeAsync: async ({ value }) => {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      return value < 13 ? 'You must be 13 to make an account' : undefined
    },
  }}
>
  {(field) => (
    <>
      <label for={field().name}>Age:</label>
      <input
        id={field().name}
        name={field().name}
        value={field().state.value}
        type="number"
        onInput={(e) => field().handleChange(e.target.valueAsNumber)}
      />
      {!field().state.meta.isValid ? (
        <em role="alert">{field().state.meta.errors.join(', ')}</em>
      ) : null}
    </>
  )}
</form.Field>

Combining Sync and Async Validation

<form.Field
  name="age"
  validators={{
    onBlur: ({ value }) => (value < 13 ? 'You must be at least 13' : undefined),
    onBlurAsync: async ({ value }) => {
      const currentAge = await fetchCurrentAgeOnProfile()
      return value < currentAge ? 'You can only increase the age' : undefined
    },
  }}
>
  {(field) => (
    <>
      <input
        value={field().state.value}
        type="number"
        onBlur={field().handleBlur}
        onInput={(e) => field().handleChange(e.target.valueAsNumber)}
      />
      {field().state.meta.errors ? (
        <em role="alert">{field().state.meta.errors.join(', ')}</em>
      ) : null}
    </>
  )}
</form.Field>
By default, the async validator only runs if the sync validator succeeds. Set asyncAlways: true to change this behavior.

Built-in Debouncing

Debounce async validations to avoid excessive API calls:
<form.Field
  name="age"
  asyncDebounceMs={500}
  validators={{
    onChangeAsync: async ({ value }) => {
      // Debounced by 500ms
    },
  }}
  children={(field) => <>{/* ... */}</>}
/>
Override per validator:
<form.Field
  name="age"
  asyncDebounceMs={500}
  validators={{
    onChangeAsyncDebounceMs: 1500,
    onChangeAsync: async ({ value }) => {
      // Debounced by 1500ms
    },
    onBlurAsync: async ({ value }) => {
      // Debounced by 500ms
    },
  }}
  children={(field) => <>{/* ... */}</>}
/>

Schema-Based Validation

TanStack Form supports the Standard Schema specification:

Using Zod

import { z } from 'zod'

const form = createForm(() => ({
  // ...
}))

<form.Field
  name="age"
  validators={{
    onChange: z.number().gte(13, 'You must be 13 to make an account'),
  }}
  children={(field) => <>{/* ... */}</>}
/>

Async Schema Validation

<form.Field
  name="age"
  validators={{
    onChange: z.number().gte(13, 'You must be 13 to make an account'),
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: z.number().refine(
      async (value) => {
        const currentAge = await fetchCurrentAgeOnProfile()
        return value >= currentAge
      },
      {
        message: 'You can only increase the age',
      },
    ),
  }}
  children={(field) => <>{/* ... */}</>}
/>

Combining Schema with Callbacks

For advanced control, combine schemas with callback functions:
<form.Field
  name="age"
  validators={{
    onChange: ({ value, fieldApi }) => {
      const errors = fieldApi.parseValueWithSchema(
        z.number().gte(13, 'You must be 13 to make an account'),
      )

      if (errors) return errors

      // Continue with custom validation
    },
  }}
  children={(field) => <>{/* ... */}</>}
/>

Form-Level Schema Example

Here’s a complete example using Zod for form-level validation:
import { createForm } from '@tanstack/solid-form'
import { z } from 'zod'

const ZodSchema = z.object({
  firstName: z
    .string()
    .min(3, 'You must have a length of at least 3')
    .startsWith('A', "First name must start with 'A'"),
  lastName: z.string().min(3, 'You must have a length of at least 3'),
})

function App() {
  const form = createForm(() => ({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    validators: {
      onChange: ZodSchema,
    },
    onSubmit: async ({ value }) => {
      console.log(value)
    },
  }))

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="firstName"
        children={(field) => (
          <>
            <label for={field().name}>First Name:</label>
            <input
              id={field().name}
              name={field().name}
              value={field().state.value}
              onInput={(e) => field().handleChange(e.target.value)}
            />
            {field().state.meta.errors.length > 0 ? (
              <em>{field().state.meta.errors.map((e) => e.message).join(', ')}</em>
            ) : null}
          </>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

Preventing Invalid Form Submission

The form state includes a canSubmit flag that prevents submission when the form is invalid:
const form = createForm(() => ({
  /* ... */
}))

return (
  <form.Subscribe
    selector={(state) => ({
      canSubmit: state.canSubmit,
      isSubmitting: state.isSubmitting,
    })}
    children={(state) => (
      <button type="submit" disabled={!state().canSubmit}>
        {state().isSubmitting ? '...' : 'Submit'}
      </button>
    )}
  />
)
To prevent submission before any user interaction, combine canSubmit with isPristine: disabled={!state().canSubmit || state().isPristine}

Next Steps