Form Groups
When building a multi-stage form that has many stages, like so:

It's common for each step to have its own form. However, this complicates the form submission and validation process by requiring you to add complex logic.
Luckily, TanStack Form provides a way to build out sub-forms that make this kind of development trivial to implement: <form.FormGroup>.
Usage#
To use a form group in TanStack Form, you'll use useForm to create a form variable, then reference its FormGroup component like you would a Field:
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
})
</script>
<template>
<form.FormGroup name="step1" v-slot="{ group: formGroup }">
<!-- `formGroup` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue` -->
<!-- ... -->
</form.FormGroup>
</template>This becomes much more useful when paired with external state to conditionally render a FormGroup:
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@tanstack/vue-form'
const step = ref(0)
const form = useForm({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
})
const onGroupSubmit = () => {
step.value++
}
const onGroupSubmitInvalid = () => {
// Or handle invalid submissions, just like a top-level form
}
</script>
<template>
<form.FormGroup
v-if="step === 0"
name="step1"
:onGroupSubmit="onGroupSubmit"
:onGroupSubmitInvalid="onGroupSubmitInvalid"
:onSubmitMeta="{} as SomeType"
v-slot="{ group: formGroup }"
>
<form @submit.prevent.stop="formGroup.handleSubmit()">
<!-- Use `formGroup.handleSubmit()` to submit the sub-form, but not the parent form -->
<!-- ... -->
</form>
</form.FormGroup>
<form.FormGroup
v-if="step === 1"
name="step2"
:onGroupSubmit="() => form.handleSubmit()"
v-slot="{ group: formGroup }"
>
<form @submit.prevent.stop="formGroup.handleSubmit()">
<!-- Then, use `form.handleSubmit()` to submit the entire form -->
<!-- ... -->
</form>
</form.FormGroup>
</template>Form Group Validation#
Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
- Form groups can have their own validation:
<template>
<form.FormGroup
name="step1"
:validators="{ onChange: () => 'Error' }"
v-slot="{ group: formGroup }"
>
<!-- formGroup.state.meta.errorMap // {onChange: "Error" | undefined} -->
<!-- formGroup.state.meta.errors // ("Error")[] -->
</form.FormGroup>
</template>- Can set errors on sub-fields:
<template>
<form.FormGroup
name="step1"
:validators="{
onChange: ({ value, groupApi }) => ({
group: value.name === 'error' ? 'Group error' : undefined,
fields: {
// Must use the name of the field relative to the FormGroup as the error key,
// to stay consistent with how standard schema works with form groups
name: value.name === 'error' ? 'Field error' : undefined,
},
}),
}"
/>
</template>- And can even accept standard schemas:
<template>
<form.FormGroup
name="step1"
:validators="{
onChange: z.object({
name: z.string().min(2),
}),
}"
/>
</template>The reason we don't use the full path names for fields is so that you can compose your schemas like so:
tsconst step1Schema = z.object({ name: z.string().min(2), }) const schema = z.object({ step1: step1Schema, step2: step2Schema, })And pass the step1Schema to a form group and schema to the parent form. That way, partially validated data will still flag errors if the group is bypassed.
Dynamic Group Validation#
If you want to use dynamic validation (onDynamic) with a form group, do not rely on the onDynamic validator passed to useForm:
useForm({
validationLogic: revalidateLogic(),
validators: {
// This validator will not run `onChange` when a sub-form is submitted;
// it will only run `onChange` when the form itself is submitted.
onDynamic: schema,
},
})Instead, pass your sub-schema for the group to the onDynamic validation of the FormGroup itself:
<template>
<form.FormGroup name="step1" :validators="{ onDynamic: step1Schema }" />
</template>It will treat formGroup.submissionAttempts as the way to change what validator is ran before/after submit.
Form Group State#
Just like you're able to access formGroup.state.meta.errors, you're also able to access the group's value using formGroup.state.value. Likewise, here are some valuable properties you can access in the formGroup.state.meta:
- formGroup.state.meta.isFieldsValid: true when the field-level validators have no errors
- formGroup.state.meta.isGroupValid: true when the group-level validators have no errors
- formGroup.state.meta.isValid: true when both the field-level and group-level validators have no errors
- formGroup.state.meta.isSubmitting: true when the group is in the process of being submitted