diff --git a/docs/config.json b/docs/config.json
index 6b289ada6..fc8bcda28 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -580,6 +580,10 @@
{
"label": "Devtools",
"to": "framework/react/examples/devtools"
+ },
+ {
+ "label": "Multistep Form",
+ "to": "framework/react/examples/multistep"
}
]
},
diff --git a/examples/react/multistep/.eslintrc.cjs b/examples/react/multistep/.eslintrc.cjs
new file mode 100644
index 000000000..35853b617
--- /dev/null
+++ b/examples/react/multistep/.eslintrc.cjs
@@ -0,0 +1,11 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
+ rules: {
+ 'react/no-children-prop': 'off',
+ },
+}
+
+module.exports = config
diff --git a/examples/react/multistep/.gitignore b/examples/react/multistep/.gitignore
new file mode 100644
index 000000000..4673b022e
--- /dev/null
+++ b/examples/react/multistep/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+pnpm-lock.yaml
+yarn.lock
+package-lock.json
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/examples/react/multistep/README.md b/examples/react/multistep/README.md
new file mode 100644
index 000000000..1cf889265
--- /dev/null
+++ b/examples/react/multistep/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install`
+- `npm run dev`
diff --git a/examples/react/multistep/index.html b/examples/react/multistep/index.html
new file mode 100644
index 000000000..5078e2046
--- /dev/null
+++ b/examples/react/multistep/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ TanStack Form React Multistep Form Example App
+
+
+
+
+
+
+
diff --git a/examples/react/multistep/package.json b/examples/react/multistep/package.json
new file mode 100644
index 000000000..d33147501
--- /dev/null
+++ b/examples/react/multistep/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@tanstack/form-example-react-multistep",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3001",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/react-form": "^1.28.5",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@tanstack/react-devtools": "^0.9.7",
+ "@tanstack/react-form-devtools": "^0.2.19",
+ "@types/react": "^19.0.7",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "vite": "^7.2.2"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/react/multistep/public/emblem-light.svg b/examples/react/multistep/public/emblem-light.svg
new file mode 100644
index 000000000..a58e69ad5
--- /dev/null
+++ b/examples/react/multistep/public/emblem-light.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/examples/react/multistep/src/index.tsx b/examples/react/multistep/src/index.tsx
new file mode 100644
index 000000000..a5a3c0780
--- /dev/null
+++ b/examples/react/multistep/src/index.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react'
+import { createRoot } from 'react-dom/client'
+import { TanStackDevtools } from '@tanstack/react-devtools'
+import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
+import { useForm } from '@tanstack/react-form';
+import { z } from 'zod';
+import type { AnyFieldApi, DeepKeys } from '@tanstack/react-form';
+
+const userSchema = z.object({
+ firstName: z.string().min(2, 'Too short'),
+ email: z.string().email('Invalid email'),
+});
+
+type FormData = z.infer;
+
+function FieldUI({ field, label }: { field: AnyFieldApi; label: string }) {
+ return (
+
+ field.handleChange(e.target.value)}
+ placeholder={label}
+ />
+ {field.state.meta.errors.length > 0 && (
+
+ {field.state.meta.errors
+ .map((e) => (typeof e === 'string' ? e : e.message))
+ .join(', ')}
+
+ )}
+
+ );
+}
+
+export default function App() {
+ const [step, setStep] = React.useState(0);
+
+ const form = useForm({
+ defaultValues: {
+ firstName: '',
+ email: '',
+ } as FormData,
+ validators: { onSubmit: userSchema },
+ onSubmit: async ({ value }) => console.log('Final submit:', value),
+ });
+
+ const steps = [
+ {
+ fields: ['firstName'] as const,
+ component: () => (
+ }
+ />
+ ),
+ },
+ {
+ fields: ['email'] as const,
+ component: () => (
+ }
+ />
+ ),
+ },
+ ];
+
+ type FormFieldName = DeepKeys;
+
+ const validateFieldGroup = async (fields: readonly FormFieldName[]) => {
+ await Promise.all(fields.map((field) => form.validateField(field, 'blur')));
+ return fields.every(
+ (field) => (form.getFieldMeta(field)?.errors.length ?? 0) === 0
+ );
+ };
+
+ const next = async () => {
+ const result = await validateFieldGroup(steps[step].fields);
+ if (result) {
+ setStep((s) => s + 1);
+ }
+ };
+
+ return (
+
+ );
+}
+
+
+
+const rootElement = document.getElementById('root')!
+
+createRoot(rootElement).render(
+
+
+
+
+ ,
+)
diff --git a/examples/react/multistep/tsconfig.json b/examples/react/multistep/tsconfig.json
new file mode 100644
index 000000000..22b43163b
--- /dev/null
+++ b/examples/react/multistep/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}