AutoFormAutoForm
TanStack Form

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.

TanStack real-time validation
"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:

  1. Render the dialog actions inside AutoForm and call useFormContext.

  2. Create a form with useAppForm, pass it as formControl, and call methods on it from outside AutoForm.

TanStack dialog submit
"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: "..." }).

TanStack custom fields form
Banana#6366f1
50
Submitted value
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.

PatternExample in the demo below
Disable a fieldGift message — visible but disabled until This is a gift is checked
Hide a fieldCoupon code — invisible until "Have a coupon?" is checked
Conditional validationPayment details — required by superRefine only when no 100% coupon is applied
Dynamic optionsState dropdown — list rebuilds every time the country changes
TanStack ecommerce checkout

Payment

"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.

TanStack multistep form
ContactAccountAddressPreferences
Submitted value
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.

TanStack nested AutoForm
Submitted value
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.

TanStack interactive form builder
Loading...
"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.

On this page