FAQ & Examples
Common TanStack Form usage patterns.
How do I show validation state in real time and disable submit until valid?
Use TanStack Form's subscription APIs. Inside AutoForm children, get the form with useFormContext, then subscribe to state.canSubmit.
"use client";
import * as React from "react";
import * as z from "zod";
import { ZodProvider } from "@autoform/zod";
import { useFormContext } from "@autoform/react/tanstack-form";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
import { Button } from "@/components/ui/button";
const realtimeSchema = z.object({
email: z.email("Enter a valid email"),
password: z.string().min(8, "Use at least 8 characters"),
});
type RealtimeValues = z.infer<typeof realtimeSchema>;
const schemaProvider = new ZodProvider(realtimeSchema);
function SubmitButton() {
const form = useFormContext();
return (
<form.Subscribe selector={(state) => state.canSubmit}>
{(canSubmit) => (
<Button type="submit" className="mt-4" disabled={!canSubmit}>
Create Account
</Button>
)}
</form.Subscribe>
);
}
export function TanStackRealtimeValidationDemo() {
const [values, setValues] = React.useState<RealtimeValues | null>(null);
return (
<div className="gap-4 rounded-lg border bg-background p-6">
<AutoForm
schema={schemaProvider}
onSubmit={(values) => setValues(values)}
>
<SubmitButton />
{values && (
<pre className="max-w-full overflow-x-auto rounded-md bg-secondary p-3 text-xs">
{JSON.stringify(values, null, 2)}
</pre>
)}
</AutoForm>
</div>
);
}
How do I submit or reset AutoForm from buttons in a dialog?
Use one of these approaches:
-
Render the dialog actions inside AutoForm and call
useFormContext. -
Create a form with
useAppForm, pass it asformControl, and call methods on it from outside AutoForm.
"use client";
import * as React from "react";
import { CheckCircle, Circle } from "lucide-react";
import * as z from "zod";
import { fieldConfig, ZodProvider } from "@autoform/zod";
import { useFormContext } from "@autoform/react/tanstack-form";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const dialogSchema = z.object({
username: z.string().min(2, "Enter at least 2 characters"),
action: z.enum(["create", "read", "update", "delete"]).check(
fieldConfig({
label: "Action",
inputProps: {
placeholder: "Select an action",
},
}),
),
});
const schemaProvider = new ZodProvider(dialogSchema);
function ResetButton() {
const form = useFormContext();
return (
<Button
type="button"
variant="outline"
className="w-full gap-2"
onClick={() => form.reset()}
>
<Circle className="h-4 w-4" />
Reset
</Button>
);
}
export default function TanStackDialogSubmitDemo() {
const [submitted, setSubmitted] = React.useState<z.infer<
typeof dialogSchema
> | null>(null);
return (
<div className="flex min-h-72 items-center justify-center rounded-lg border bg-background p-6">
<Dialog>
<DialogTrigger asChild>
<Button>Open dialog form</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Edit permission</DialogTitle>
<DialogDescription>
The submit and reset buttons are rendered as AutoForm children.
</DialogDescription>
</DialogHeader>
<AutoForm
schema={schemaProvider}
onSubmit={(data, form) => {
setSubmitted(data);
form.reset();
}}
defaultValues={{ username: "", action: "" }}
>
<Button type="submit" className="w-full gap-2">
<CheckCircle className="h-4 w-4" />
Save
</Button>
<ResetButton />
</AutoForm>
{submitted && (
<pre className="overflow-auto rounded-md bg-secondary p-3 text-xs">
{JSON.stringify(submitted, null, 2)}
</pre>
)}
</DialogContent>
</Dialog>
</div>
);
}
How do I add custom fields?
Register the custom input in formComponents, read the current field with useFieldContext, and mark the matching schema field with fieldConfig({ fieldType: "..." }).
null
"use client";
import * as React from "react";
import { fieldConfig, ZodProvider } from "@autoform/zod";
import * as z from "zod";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
import {
ColorPickerField,
DatePickerField,
FileUploadField,
NumberStepperField,
RadioCardField,
SliderField,
} from "@/components/tanstack-custom-field-components";
const schema = z.object({
quantity: z
.number()
.min(1)
.max(99)
.default(1)
.describe("Quantity")
.check(fieldConfig({ fieldType: "numberStepper" })),
themeColor: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Pick a valid color")
.default("#6366f1")
.describe("Find the color")
.check(fieldConfig({ fieldType: "colorPicker" })),
birthdate: z
.string()
.min(1, "Pick a date")
.describe("Birthdate")
.check(fieldConfig({ fieldType: "dateTime" })),
avatar: z
.instanceof(File)
.optional()
.describe("Profile picture")
.check(fieldConfig({ fieldType: "fileUpload" })),
volume: z
.number()
.min(0)
.max(100)
.default(50)
.check(fieldConfig({ fieldType: "slider" })),
plan: z
.enum(["starter", "pro", "enterprise"])
.default("starter")
.describe("Plan")
.check(
fieldConfig({
fieldType: "radioCard",
customData: {
options: [
{ id: "starter", label: "Starter", desc: "Up to 3 projects" },
{ id: "pro", label: "Pro", desc: "Up to 100 projects" },
{ id: "enterprise", label: "Enterprise", desc: "Custom limits" },
],
},
}),
),
});
const schemaProvider = new ZodProvider(schema);
type FormValues = z.infer<typeof schema>;
export function TanStackCustomFieldsDemo() {
const [result, setResult] = React.useState<FormValues | null>(null);
return (
<div className="space-y-6 rounded-lg border bg-background p-6">
<AutoForm
schema={schemaProvider}
formComponents={{
numberStepper: NumberStepperField,
radioCard: RadioCardField,
dateTime: DatePickerField,
slider: SliderField,
colorPicker: ColorPickerField,
fileUpload: FileUploadField,
}}
formProps={{ className: "flex flex-col gap-6" }}
onSubmit={(data) => setResult(data)}
withSubmit
/>
<div className="rounded-md border bg-secondary/50 p-4 text-sm">
<div className="mb-2 font-medium">Submitted value</div>
<pre className="overflow-auto text-xs">
{result
? JSON.stringify(
{
quantity: result.quantity,
plan: result.plan,
birthdate: result.birthdate,
volume: result.volume,
themeColor: result.themeColor,
avatar: result.avatar?.name ?? null,
},
null,
2,
)
: "null"}
</pre>
</div>
</div>
);
}
How do I create fields with dependencies between them?
Use schema validation for data rules, and TanStack Form subscriptions for UI behavior. Custom fields and wrappers can subscribe to related values with form.Subscribe or useStore.
In most field components, prefer form.Subscribe because it keeps the reactive part local to the value it reads.
| Pattern | Example in the demo below |
|---|---|
| Disable a field | Gift message — visible but disabled until This is a gift is checked |
| Hide a field | Coupon code — invisible until "Have a coupon?" is checked |
| Conditional validation | Payment details — required by superRefine only when no 100% coupon is applied |
| Dynamic options | State dropdown — list rebuilds every time the country changes |
"use client";
import * as React from "react";
import * as z from "zod";
import { fieldConfig, ZodProvider } from "@autoform/zod";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
import {
CountrySelectField,
CouponCodeFieldWrapper,
GiftMessageField,
PaymentFieldWrapper,
StateSelectField,
} from "@/components/tanstack-ecommerce-checkout-fields";
const schema = z
.object({
country: z
.string()
.min(1, "Country is required")
.describe("Country")
.check(fieldConfig({ fieldType: "country" })),
state: z
.string()
.min(1, "State / province is required")
.describe("State / Province")
.check(fieldConfig({ fieldType: "state" })),
haveCoupon: z
.boolean()
.optional()
.default(false)
.describe("Have a coupon?"),
couponCode: z
.string()
.optional()
.describe("Coupon code")
.check(
fieldConfig({
fieldWrapper: CouponCodeFieldWrapper,
inputProps: { placeholder: "Try free100" },
}),
),
isGift: z.boolean().optional().default(false).describe("This is a gift"),
giftMessage: z
.string()
.optional()
.describe("Gift message")
.check(fieldConfig({ fieldType: "giftMessage" })),
payment: z
.object({
card: z
.string()
.optional()
.describe("Card")
.check(
fieldConfig({ inputProps: { placeholder: "1234 5678 9012 3456" } }),
),
expiryDate: z
.string()
.optional()
.describe("Expiry (MM/YY)")
.check(fieldConfig({ inputProps: { placeholder: "MM/YY" } })),
code: z
.string()
.optional()
.describe("Code")
.check(fieldConfig({ inputProps: { placeholder: "123" } })),
})
.check(fieldConfig({ fieldWrapper: PaymentFieldWrapper })),
})
.superRefine((data, ctx) => {
if (data.haveCoupon && !data.couponCode?.trim()) {
ctx.addIssue({
code: "custom",
message: "Coupon code is required",
path: ["couponCode"],
});
}
const isFree =
data.haveCoupon && data.couponCode?.toUpperCase() === "FREE100";
if (!isFree) {
if (!data.payment?.card?.trim()) {
ctx.addIssue({
code: "custom",
message: "Payment card is required",
path: ["payment", "card"],
});
}
if (!data.payment?.expiryDate?.trim()) {
ctx.addIssue({
code: "custom",
message: "Expiry date is required",
path: ["payment", "expiryDate"],
});
}
if (!data.payment?.code?.trim()) {
ctx.addIssue({
code: "custom",
message: "Code is required",
path: ["payment", "code"],
});
}
}
});
type CheckoutValues = z.infer<typeof schema>;
const schemaProvider = new ZodProvider(schema);
export function TanStackEcommerceCheckoutDemo() {
const [result, setResult] = React.useState<CheckoutValues | null>(null);
return (
<div className="space-y-6 rounded-lg border bg-background p-6">
<AutoForm
schema={schemaProvider}
formComponents={{
country: CountrySelectField,
state: StateSelectField,
giftMessage: GiftMessageField,
}}
onSubmit={(data) => setResult(data as CheckoutValues)}
withSubmit
/>
{result && (
<div className="rounded-md border bg-secondary/50 p-4 text-sm">
<div className="mb-2 font-medium">Submitted value</div>
<pre className="overflow-auto text-xs">
{JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
);
}
How do I create a simple multistep form?
Use separate schema for each step. Render the current step, save each submitted step value in parent state, then move to the next step.
null
"use client";
import * as React from "react";
import * as z from "zod";
import {
TanStackMultistepForm,
} from "@/components/tanstack-multistep-form";
import type { TanStackMultistepFormStep } from "@/components/tanstack-multistep-form";
const STEPS: TanStackMultistepFormStep[] = [
{
label: "Contact",
schema: z.object({
fullName: z.string().min(2, "Enter your name"),
email: z.email("Enter a valid email"),
}),
defaults: { fullName: "", email: "" },
},
{
label: "Account",
schema: z.object({
username: z.string().min(3, "Use at least 3 characters"),
password: z.string().min(8, "Use at least 8 characters"),
}),
defaults: { username: "", password: "" },
},
{
label: "Address",
schema: z.object({
street: z.string().min(3, "Enter your street address"),
city: z.string().min(2, "Enter your city"),
}),
defaults: { street: "", city: "" },
},
{
label: "Preferences",
schema: z.object({
newsletter: z.boolean().default(false),
notes: z.string().min(5, "Add a short note"),
}),
defaults: { newsletter: false, notes: "" },
},
];
export default function TanStackMultistepFormDemo() {
const [submitted, setSubmitted] = React.useState<Record<
number,
Record<string, unknown>
> | null>(null);
return (
<div className="grid gap-4 rounded-lg border bg-background p-6 md:grid-cols-[1fr_220px]">
<TanStackMultistepForm steps={STEPS} onSubmit={setSubmitted} />
<div className="rounded-md border bg-secondary/50 p-4 text-sm">
<div className="font-medium">Submitted value</div>
<pre className="mt-3 overflow-auto text-xs">
{JSON.stringify(submitted, null, 2)}
</pre>
</div>
</div>
);
}
How do I use another AutoForm inside an AutoForm field?
Create a custom field component, register it in formComponents, and pass the nested AutoForm submit value back with the current field's onChange handler.
null
"use client";
import * as React from "react";
import type { AutoFormFieldProps } from "@autoform/react";
import { useFieldContext } from "@autoform/react/tanstack-form";
import { fieldConfig, ZodProvider } from "@autoform/zod";
import * as z from "zod";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const colorsSchema = z.object({
colors: z.array(z.string().min(1, "Enter a color")).min(1),
});
const profileSchema = z.object({
username: z.string().min(2, "Enter a username"),
colors: z
.array(z.string())
.min(1, "Choose at least one color")
.default([])
.describe("Favorite colors")
.check(fieldConfig({ fieldType: "colorDialog" })),
});
const colorsProvider = new ZodProvider(colorsSchema);
const profileProvider = new ZodProvider(profileSchema);
function ColorDialogField() {
const [open, setOpen] = React.useState(false);
const field = useFieldContext<string[]>();
const colors = Array.isArray(field.state.value) ? field.state.value : [];
return (
<div className="space-y-3">
{colors.length > 0 && (
<div className="flex flex-wrap gap-2">
{colors.map((color, index) => (
<span
className="rounded-md border bg-secondary px-2 py-1 text-xs"
key={`${color}-${index}`}
>
{color}
</span>
))}
</div>
)}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="outline" size="sm">
Add colors
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Pick favorite colors</DialogTitle>
<DialogDescription>
This nested AutoForm writes its value back to the parent field.
</DialogDescription>
</DialogHeader>
<AutoForm
schema={colorsProvider}
values={{ colors }}
onSubmit={(data) => {
field.handleChange(data.colors);
setOpen(false);
}}
>
<Button type="submit">Save Colors</Button>
</AutoForm>
</DialogContent>
</Dialog>
</div>
);
}
export default function TanStackNestedAutoFormDemo() {
const [result, setResult] = React.useState<z.infer<
typeof profileSchema
> | null>(null);
return (
<div className="grid gap-4 rounded-lg border bg-background p-6 md:grid-cols-[1fr_220px]">
<AutoForm
schema={profileProvider}
formComponents={{
colorDialog: ColorDialogField as React.FC<AutoFormFieldProps>,
}}
onSubmit={(data) => setResult(data)}
withSubmit
/>
<div className="rounded-md border bg-secondary/50 p-4 text-sm">
<div className="font-medium">Submitted value</div>
<pre className="mt-3 overflow-auto text-xs">
{JSON.stringify(result, null, 2)}
</pre>
</div>
</div>
);
}
How do I create an interactive, dynamic form builder?
Combine a Monaco editor with AutoForm for interactive forms: evaluate the Zod schema string on every editor change and pass the resulting ZodProvider to AutoForm.
"use client";
import React, { useEffect, useState } from "react";
import Editor from "@monaco-editor/react";
import { z } from "zod";
import { ZodProvider } from "@autoform/zod";
import type { SchemaProvider } from "@autoform/core";
import { AutoForm } from "@/components/ui/autoform/tanstack-form";
const editorOptions = {
minimap: { enabled: false },
scrollBeyondLastLine: false,
lineNumbersMinChars: 2,
glyphMargin: false,
folding: false,
scrollbar: {
useShadows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
alwaysConsumeMouseWheel: false,
},
};
const getEditorTheme = () =>
typeof document === "undefined"
? "light"
: document.documentElement.classList.contains("dark")
? "vs-dark"
: "light";
function useEditorTheme() {
const [theme, setTheme] = useState(getEditorTheme);
useEffect(() => {
const observer = new MutationObserver(() => setTheme(getEditorTheme()));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
return theme;
}
const defaultCode = `z.object({
name: z.string(),
age: z.coerce.number(),
isHuman: z.boolean(),
})`;
const globalZod = z;
export default function TanStackInteractiveSchemaDemoContent() {
const [code, setCode] = React.useState(defaultCode);
const [schemaProvider, setSchemaProvider] = React.useState<SchemaProvider>(
() =>
new ZodProvider(
z.object({
name: z.string(),
age: z.coerce.number(),
isHuman: z.boolean(),
}),
),
);
const [formKey, setFormKey] = React.useState(0);
const [data, setData] = useState("");
const editorTheme = useEditorTheme();
useEffect(() => {
try {
const z = globalZod;
const parsedSchema = eval(code);
const provider = new ZodProvider(parsedSchema);
provider.parseSchema();
setSchemaProvider(provider);
setFormKey((key) => key + 1);
} catch (error) {
console.error(error);
}
}, [code]);
return (
<div className="grid w-full gap-1 overflow-hidden rounded-lg border bg-background md:grid-cols-2">
<div className="border-b bg-muted/40 p-1 md:border-b-0 md:border-r md:px-1 md:py-4">
<Editor
className="h-65 border md:h-95 md:border-0"
options={editorOptions}
defaultLanguage="javascript"
defaultValue={defaultCode}
theme={editorTheme}
onChange={(value) => setCode(value || "")}
/>
</div>
<div className="p-6 pb-8">
<AutoForm
key={formKey}
schema={schemaProvider}
onSubmit={(data) => setData(JSON.stringify(data, null, 2))}
withSubmit
/>
{data && (
<pre className="mt-4 overflow-auto rounded-md bg-muted p-4 text-sm">
{data}
</pre>
)}
</div>
</div>
);
}
Caution
This demo uses eval() to parse the editor input. Avoid using eval() with
untrusted input or in production server-side code.