diff --git a/.changeset/tiny-badgers-swim.md b/.changeset/tiny-badgers-swim.md
new file mode 100644
index 000000000..d2a39c0ff
--- /dev/null
+++ b/.changeset/tiny-badgers-swim.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/svelte-form': minor
+---
+
+Add createFormCreator API
diff --git a/docs/framework/svelte/guides/form-composition.md b/docs/framework/svelte/guides/form-composition.md
new file mode 100644
index 000000000..f9fbc0dda
--- /dev/null
+++ b/docs/framework/svelte/guides/form-composition.md
@@ -0,0 +1,507 @@
+---
+id: form-composition
+title: Form Composition
+---
+
+A common criticism of TanStack Form is its verbosity out-of-the-box. While this _can_ be useful for educational purposes - helping enforce understanding our APIs - it's not ideal in production use cases.
+
+As a result, while `form.Field` enables the most powerful and flexible usage of TanStack Form, we provide APIs that wrap it and make your application code less verbose.
+
+## Custom Form Runes
+
+The most powerful way to compose forms is to create custom form runes. This allows you to create a form rune that is tailored to your application's needs, including pre-bound custom UI components and more.
+
+At its most basic, `createFormCreator` is a function that returns a `createAppForm` rune.
+
+> This un-customized `createAppForm` rune is identical to `createForm`, but that will quickly change as we add more options to `createFormCreator`.
+
+```ts
+// form-context.ts
+import { createFormCreatorContexts } from '@tanstack/svelte-form'
+
+// export useFieldContext and useFormContext for use in your custom components
+export const { useFieldContext, useFormContext } = createFormCreatorContexts()
+```
+
+```ts
+// form.ts
+import { createFormCreator } from '@tanstack/svelte-form'
+
+export const { createAppForm } = createFormCreator({
+ // We'll learn more about these options later
+ fieldComponents: {},
+ formComponents: {},
+})
+```
+
+```svelte
+
+
+
+
+
+
+```
+
+### Pre-bound Field Components
+
+Once this scaffolding is in place, you can start adding custom field and form components to your form rune.
+
+> Note: the `useFieldContext` must be the same one exported from your custom form context
+
+```svelte
+
+
+
+
+ {label}
+ field.handleChange((e.target as HTMLInputElement).value)}
+ />
+
+```
+
+You're then able to register this component with your form rune.
+
+```ts
+import { createFormCreator } from '@tanstack/svelte-form'
+import TextField from './text-field.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {},
+})
+```
+
+And use it in your form:
+
+```svelte
+
+
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+```
+
+This not only allows you to reuse the UI of your shared component, but retains the type-safety you'd expect from TanStack Form: Typo `name` and get a TypeScript error.
+
+### Pre-bound Form Components
+
+While `form.AppField` solves many of the problems with Field boilerplate and reusability, it doesn't solve the problem of _form_ boilerplate and reusability.
+
+In particular, being able to share instances of `form.Subscribe` for, say, a reactive form submission button is a common usecase.
+
+```svelte
+
+
+
+ state.isSubmitting}>
+ {#snippet children(isSubmitting)}
+
+ {label}
+
+ {/snippet}
+
+```
+
+```ts
+// form.ts
+import { createFormCreator } from '@tanstack/svelte-form'
+import TextField from './text-field.svelte'
+import SubscribeButton from './subscribe-button.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+})
+```
+
+```svelte
+
+
+
+
+
+ {#snippet children()}
+
+ {/snippet}
+
+```
+
+## Breaking big forms into smaller pieces
+
+Sometimes forms get very large; it's just how it goes sometimes. While TanStack Form supports large forms well, it's never fun to work with hundreds or thousands of lines of code long files.
+
+To solve this, you can break forms into smaller Svelte components that accept the form as a prop.
+
+```ts
+// shared-form.ts
+import { formOptions } from '@tanstack/svelte-form'
+
+export const peopleFormOpts = formOptions({
+ defaultValues: {
+ firstName: 'John',
+ lastName: 'Doe',
+ },
+})
+```
+
+```svelte
+
+
+
+
+
{title}
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children()}
+
+ {/snippet}
+
+
+```
+
+```svelte
+
+
+
+
+```
+
+## Reusing groups of fields in multiple forms
+
+Sometimes, a pair of fields are so closely related that it makes sense to group and reuse them — like the password example listed in the [linked fields guide](../linked-fields.md). Instead of repeating this logic across multiple forms, you can create reusable field group components.
+
+> Unlike form-level components, validators in field groups cannot be strictly typed and could be any value.
+> Ensure that your fields can accept unknown error types.
+
+Rewriting the passwords example as a reusable component would look like this:
+
+```svelte
+
+
+
+
+
{title}
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
{
+ if (value !== fieldApi.form.getFieldValue('password')) {
+ return 'Passwords do not match'
+ }
+ return undefined
+ },
+ }}
+ >
+ {#snippet children(field)}
+
+
+ {#each field.state.meta.errors as error}
+
{error}
+ {/each}
+
+ {/snippet}
+
+
+```
+
+We can now use these grouped fields in any form that implements the required fields:
+
+```svelte
+
+
+
+
+ {#snippet children()}
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+
+ {/snippet}
+
+```
+
+## Tree-shaking form and field components
+
+While the above examples are great for getting started, they're not ideal for certain use-cases where you might have hundreds of form and field components.
+In particular, you may not want to include all of your form and field components in the bundle of every file that uses your form rune.
+
+To solve this, you can use dynamic imports with Svelte's component loading:
+
+```ts
+// src/runes/form-context.ts
+import { createFormCreatorContexts } from '@tanstack/svelte-form'
+
+export const { useFieldContext, useFormContext } = createFormCreatorContexts()
+```
+
+```svelte
+
+
+
+
+ {label}
+ field.handleChange((e.target as HTMLInputElement).value)}
+ />
+
+```
+
+```ts
+// src/runes/form.ts
+import { createFormCreator } from '@tanstack/svelte-form'
+import TextField from '../components/text-field.svelte'
+import SubscribeButton from '../components/subscribe-button.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+})
+```
+
+```svelte
+
+
+
+
+```
+
+## Putting it all together
+
+Now that we've covered the basics of creating custom form runes, let's put it all together in a single example.
+
+```ts
+// /src/runes/form-context.ts, to be used across the entire app
+import { createFormCreatorContexts } from '@tanstack/svelte-form'
+
+export const { useFieldContext, useFormContext } = createFormCreatorContexts()
+```
+
+```svelte
+
+
+
+
+ {label}
+ field.handleChange((e.target as HTMLInputElement).value)}
+ />
+
+```
+
+```svelte
+
+
+
+ state.isSubmitting}>
+ {#snippet children(isSubmitting)}
+
+ {label}
+
+ {/snippet}
+
+```
+
+```ts
+// /src/runes/form.ts
+import { createFormCreator } from '@tanstack/svelte-form'
+import TextField from '../components/text-field.svelte'
+import SubscribeButton from '../components/subscribe-button.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+})
+```
+
+```ts
+// /src/features/people/shared-form.ts, to be used across `people` features
+import { formOptions } from '@tanstack/svelte-form'
+
+export const peopleFormOpts = formOptions({
+ defaultValues: {
+ firstName: 'John',
+ lastName: 'Doe',
+ },
+})
+```
+
+```svelte
+
+
+
+
+
{title}
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children()}
+
+ {/snippet}
+
+
+```
+
+```svelte
+
+
+
+
+```
+
+## API Usage Guidance
+
+Here's a chart to help you decide what APIs you should be using:
+
+
diff --git a/examples/svelte/large-form/.gitignore b/examples/svelte/large-form/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/examples/svelte/large-form/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/svelte/large-form/README.md b/examples/svelte/large-form/README.md
new file mode 100644
index 000000000..1cf889265
--- /dev/null
+++ b/examples/svelte/large-form/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install`
+- `npm run dev`
diff --git a/examples/svelte/large-form/index.html b/examples/svelte/large-form/index.html
new file mode 100644
index 000000000..b6c5f0afa
--- /dev/null
+++ b/examples/svelte/large-form/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + Svelte + TS
+
+
+
+
+
+
diff --git a/examples/svelte/large-form/package.json b/examples/svelte/large-form/package.json
new file mode 100644
index 000000000..78674fb15
--- /dev/null
+++ b/examples/svelte/large-form/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@tanstack/form-example-svelte-large-form",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/svelte-form": "^1.23.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.1.1",
+ "@tsconfig/svelte": "^5.0.5",
+ "svelte": "^5.39.4",
+ "typescript": "5.8.2",
+ "vite": "^7.2.2"
+ }
+}
diff --git a/examples/svelte/large-form/src/App.svelte b/examples/svelte/large-form/src/App.svelte
new file mode 100644
index 000000000..3d5f2699a
--- /dev/null
+++ b/examples/svelte/large-form/src/App.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/examples/svelte/large-form/src/components/subscribe-button.svelte b/examples/svelte/large-form/src/components/subscribe-button.svelte
new file mode 100644
index 000000000..b1970abb0
--- /dev/null
+++ b/examples/svelte/large-form/src/components/subscribe-button.svelte
@@ -0,0 +1,13 @@
+
+
+ state.isSubmitting}>
+ {#snippet children(isSubmitting)}
+ {label}
+ {/snippet}
+
diff --git a/examples/svelte/large-form/src/components/text-field.svelte b/examples/svelte/large-form/src/components/text-field.svelte
new file mode 100644
index 000000000..411ce840f
--- /dev/null
+++ b/examples/svelte/large-form/src/components/text-field.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+ {label}
+ field.handleChange((e.target as HTMLInputElement).value)}
+ />
+
+ {#each field.state.meta.errors as error}
+
{error}
+ {/each}
+
diff --git a/examples/svelte/large-form/src/features/people/address-fields.svelte b/examples/svelte/large-form/src/features/people/address-fields.svelte
new file mode 100644
index 000000000..5807e7e44
--- /dev/null
+++ b/examples/svelte/large-form/src/features/people/address-fields.svelte
@@ -0,0 +1,37 @@
+
+
+
+
Address
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
diff --git a/examples/svelte/large-form/src/features/people/emergency-contact.svelte b/examples/svelte/large-form/src/features/people/emergency-contact.svelte
new file mode 100644
index 000000000..132c73e79
--- /dev/null
+++ b/examples/svelte/large-form/src/features/people/emergency-contact.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
diff --git a/examples/svelte/large-form/src/features/people/page.svelte b/examples/svelte/large-form/src/features/people/page.svelte
new file mode 100644
index 000000000..a72e514d3
--- /dev/null
+++ b/examples/svelte/large-form/src/features/people/page.svelte
@@ -0,0 +1,70 @@
+
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+ Emergency Contact
+
+
+ {#snippet children()}
+
+ {/snippet}
+
+
diff --git a/examples/svelte/large-form/src/features/people/shared-form.ts b/examples/svelte/large-form/src/features/people/shared-form.ts
new file mode 100644
index 000000000..05d697aed
--- /dev/null
+++ b/examples/svelte/large-form/src/features/people/shared-form.ts
@@ -0,0 +1,20 @@
+import { formOptions } from '@tanstack/svelte-form'
+
+export const peopleFormOpts = formOptions({
+ defaultValues: {
+ fullName: '',
+ email: '',
+ phone: '',
+ address: {
+ line1: '',
+ line2: '',
+ city: '',
+ state: '',
+ zip: '',
+ },
+ emergencyContact: {
+ fullName: '',
+ phone: '',
+ },
+ },
+})
diff --git a/examples/svelte/large-form/src/main.ts b/examples/svelte/large-form/src/main.ts
new file mode 100644
index 000000000..928b6c527
--- /dev/null
+++ b/examples/svelte/large-form/src/main.ts
@@ -0,0 +1,8 @@
+import { mount } from 'svelte'
+import App from './App.svelte'
+
+const app = mount(App, {
+ target: document.getElementById('app')!,
+})
+
+export default app
diff --git a/examples/svelte/large-form/src/runes/form-context.ts b/examples/svelte/large-form/src/runes/form-context.ts
new file mode 100644
index 000000000..40989db7f
--- /dev/null
+++ b/examples/svelte/large-form/src/runes/form-context.ts
@@ -0,0 +1,3 @@
+import { createFormCreatorContexts } from '@tanstack/svelte-form'
+
+export const { useFieldContext, useFormContext } = createFormCreatorContexts()
diff --git a/examples/svelte/large-form/src/runes/form.ts b/examples/svelte/large-form/src/runes/form.ts
new file mode 100644
index 000000000..fe4e06b39
--- /dev/null
+++ b/examples/svelte/large-form/src/runes/form.ts
@@ -0,0 +1,12 @@
+import { createFormCreator } from '@tanstack/svelte-form'
+import TextField from '../components/text-field.svelte'
+import SubscribeButton from '../components/subscribe-button.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+})
diff --git a/examples/svelte/large-form/src/vite-env.d.ts b/examples/svelte/large-form/src/vite-env.d.ts
new file mode 100644
index 000000000..4078e7476
--- /dev/null
+++ b/examples/svelte/large-form/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/examples/svelte/large-form/svelte.config.js b/examples/svelte/large-form/svelte.config.js
new file mode 100644
index 000000000..b0683fd24
--- /dev/null
+++ b/examples/svelte/large-form/svelte.config.js
@@ -0,0 +1,7 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+
+export default {
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
+}
diff --git a/examples/svelte/large-form/tsconfig.json b/examples/svelte/large-form/tsconfig.json
new file mode 100644
index 000000000..55a2f9b65
--- /dev/null
+++ b/examples/svelte/large-form/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ /**
+ * Typecheck JS in `.svelte` and `.js` files by default.
+ * Disable checkJs if you'd like to use dynamic types in JS.
+ * Note that setting allowJs false does not prevent the use
+ * of JS in `.svelte` files.
+ */
+ "allowJs": true,
+ "checkJs": true,
+ "isolatedModules": true,
+ "moduleDetection": "force"
+ },
+ "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/examples/svelte/large-form/vite.config.ts b/examples/svelte/large-form/vite.config.ts
new file mode 100644
index 000000000..d32eba1d6
--- /dev/null
+++ b/examples/svelte/large-form/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [svelte()],
+})
diff --git a/packages/svelte-form/src/AppField.svelte b/packages/svelte-form/src/AppField.svelte
new file mode 100644
index 000000000..fb9ab6abe
--- /dev/null
+++ b/packages/svelte-form/src/AppField.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+ {#snippet children(field: any)}
+
+ {/snippet}
+
diff --git a/packages/svelte-form/src/AppForm.svelte b/packages/svelte-form/src/AppForm.svelte
new file mode 100644
index 000000000..1ec4833dc
--- /dev/null
+++ b/packages/svelte-form/src/AppForm.svelte
@@ -0,0 +1,16 @@
+
+
+
+{@render children?.()}
diff --git a/packages/svelte-form/src/InnerAppField.svelte b/packages/svelte-form/src/InnerAppField.svelte
new file mode 100644
index 000000000..655e7ecd3
--- /dev/null
+++ b/packages/svelte-form/src/InnerAppField.svelte
@@ -0,0 +1,17 @@
+
+
+
+{@render children?.(Object.assign(field, fieldComponents))}
diff --git a/packages/svelte-form/src/context-keys.ts b/packages/svelte-form/src/context-keys.ts
new file mode 100644
index 000000000..7903532e9
--- /dev/null
+++ b/packages/svelte-form/src/context-keys.ts
@@ -0,0 +1,2 @@
+export const fieldContextKey = '__tanstack_field_context_key'
+export const formContextKey = '__tanstack_form_context_key'
diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts
index 4de61273f..5bd92181c 100644
--- a/packages/svelte-form/src/createForm.svelte.ts
+++ b/packages/svelte-form/src/createForm.svelte.ts
@@ -179,6 +179,51 @@ export interface SvelteFormApi<
WithoutFunction
}
+/**
+ * An extended version of the `FormApi` class that includes Svelte-specific functionalities from `SvelteFormApi`
+ */
+export type SvelteFormExtendedApi<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnDynamic extends undefined | FormValidateOrFn,
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+> = FormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta
+> &
+ SvelteFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta
+ >
+
export function createForm<
TParentData,
TFormOnMount extends undefined | FormValidateOrFn,
@@ -241,10 +286,10 @@ export function createForm<
// @ts-expect-error constructor definition exists only on a type level
extendedApi.Field = (internal, props) =>
- Field(internal, { ...props, form: api })
+ Field(internal, { ...props, form: api as never } as never)
extendedApi.createField = (props) =>
createField(() => {
- return { ...props(), form: api }
+ return { ...props(), form: api } as never
}) as never // Type cast because else "Error: Type instantiation is excessively deep and possibly infinite."
extendedApi.useStore = (selector) => useStore(api.store, selector)
// @ts-expect-error constructor definition exists only on a type level
diff --git a/packages/svelte-form/src/createFormCreator.svelte.ts b/packages/svelte-form/src/createFormCreator.svelte.ts
new file mode 100644
index 000000000..0637168f9
--- /dev/null
+++ b/packages/svelte-form/src/createFormCreator.svelte.ts
@@ -0,0 +1,273 @@
+import { getContext } from 'svelte'
+import { createForm } from './createForm.svelte'
+import AppFormSvelte from './AppForm.svelte'
+import AppFieldSvelte from './AppField.svelte'
+import { fieldContextKey, formContextKey } from './context-keys.js'
+import type {
+ AnyFieldApi,
+ AnyFormApi,
+ FieldApi,
+ FormAsyncValidateOrFn,
+ FormOptions,
+ FormValidateOrFn,
+} from '@tanstack/form-core'
+import type { FieldComponent } from './types.js'
+import type { SvelteFormExtendedApi } from './createForm.svelte'
+import type { Component, Snippet, SvelteComponent } from 'svelte'
+
+/**
+ * TypeScript inferencing is weird.
+ *
+ * If you have:
+ *
+ * @example
+ *
+ * interface Args {
+ * arg?: T
+ * }
+ *
+ * function test(arg?: Partial>): T {
+ * return 0 as any;
+ * }
+ *
+ * const a = test({});
+ *
+ * Then `T` will default to `unknown`.
+ *
+ * However, if we change `test` to be:
+ *
+ * @example
+ *
+ * function test(arg?: Partial>): T;
+ *
+ * Then `T` becomes `undefined`.
+ *
+ * Here, we are checking if the passed type `T` extends `DefaultT` and **only**
+ * `DefaultT`, as if that's the case we assume that inferencing has not occured.
+ */
+type UnwrapOrAny = [unknown] extends [T] ? any : T
+type UnwrapDefaultOrAny = [DefaultT] extends [T]
+ ? [T] extends [DefaultT]
+ ? any
+ : T
+ : T
+
+export function createFormCreatorContexts() {
+ function useFieldContext() {
+ const field = getContext(fieldContextKey) as AnyFieldApi
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!field) {
+ throw new Error(
+ '`fieldContext` only works when within a `fieldComponent` passed to `createFormCreator`',
+ )
+ }
+
+ return field as FieldApi<
+ any,
+ string,
+ TData,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+ >
+ }
+
+ function useFormContext() {
+ const form = getContext(formContextKey) as AnyFormApi
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!form) {
+ throw new Error(
+ '`formContext` only works when within a `formComponent` passed to `createFormCreator`',
+ )
+ }
+
+ return form as SvelteFormExtendedApi<
+ // If you need access to the form data, you need to use `withForm` instead
+ Record,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+ >
+ }
+
+ return { useFieldContext, useFormContext }
+}
+
+interface CreateFormRuneProps<
+ TFieldComponents extends Record>,
+ TFormComponents extends Record>,
+> {
+ fieldComponents: TFieldComponents
+ formComponents: TFormComponents
+}
+
+/**
+ * @private
+ */
+type AppFieldExtendedSvelteFormApi<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnDynamic extends undefined | FormValidateOrFn,
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+ TFieldComponents extends Record>,
+ TFormComponents extends Record>,
+> = SvelteFormExtendedApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta
+> &
+ NoInfer & {
+ AppField: FieldComponent<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta,
+ NoInfer
+ >
+ AppForm: Component<{ children: Snippet }>
+ }
+
+export function createFormCreator<
+ const TComponents extends Record>,
+ const TFormComponents extends Record>,
+>({
+ fieldComponents,
+ formComponents,
+}: CreateFormRuneProps) {
+ function createAppForm<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnDynamic extends undefined | FormValidateOrFn,
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+ >(
+ props: () => FormOptions<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta
+ >,
+ ): AppFieldExtendedSvelteFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents,
+ TFormComponents
+ > {
+ const form = createForm(props)
+
+ const AppForm = ((internal, props) => {
+ return AppFormSvelte(internal, { ...props, form })
+ }) as Component<{ children: Snippet }>
+
+ const AppField = ((internal, { children, ...fieldProps }) =>
+ AppFieldSvelte(internal, {
+ fieldProps,
+ form,
+ fieldComponents,
+ children,
+ } as never)) as FieldComponent<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents
+ >
+
+ const extendedForm = Object.assign(form, {
+ AppField,
+ AppForm,
+ ...formComponents,
+ })
+
+ return extendedForm
+ }
+
+ return {
+ createAppForm,
+ }
+}
diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts
index a76cbb7f2..5540f0b85 100644
--- a/packages/svelte-form/src/index.ts
+++ b/packages/svelte-form/src/index.ts
@@ -7,3 +7,8 @@ export { createForm, type SvelteFormApi } from './createForm.svelte.js'
export { default as Field, createField } from './Field.svelte'
export type { CreateField, FieldComponent } from './types.js'
+
+export {
+ createFormCreator,
+ createFormCreatorContexts,
+} from './createFormCreator.svelte.js'
diff --git a/packages/svelte-form/src/types.ts b/packages/svelte-form/src/types.ts
index cfb9d587c..853834d0a 100644
--- a/packages/svelte-form/src/types.ts
+++ b/packages/svelte-form/src/types.ts
@@ -123,6 +123,7 @@ export type FieldComponent<
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
TFormOnServer extends undefined | FormAsyncValidateOrFn,
TParentSubmitMeta,
+ ExtendedApi = {},
> =
// This giant type allows the type
// - to be used as a function (which they are now in Svelte 5)
@@ -178,7 +179,8 @@ export type FieldComponent<
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
- TParentSubmitMeta
+ TParentSubmitMeta,
+ ExtendedApi
>,
'form'
>,
@@ -235,7 +237,8 @@ export type FieldComponent<
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
- TParentSubmitMeta
+ TParentSubmitMeta,
+ ExtendedApi
>,
'form'
>
@@ -275,6 +278,7 @@ type FieldComponentProps<
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
TFormOnServer extends undefined | FormAsyncValidateOrFn,
TParentSubmitMeta,
+ ExtendedApi = {},
> = {
children: Snippet<
[
@@ -302,7 +306,8 @@ type FieldComponentProps<
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
- >,
+ > &
+ ExtendedApi,
]
>
} & Omit<
diff --git a/packages/svelte-form/tests/large-components/context.ts b/packages/svelte-form/tests/large-components/context.ts
new file mode 100644
index 000000000..944f134a6
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/context.ts
@@ -0,0 +1,3 @@
+import { createFormCreatorContexts } from '../../src/index.js'
+
+export const { useFieldContext } = createFormCreatorContexts()
diff --git a/packages/svelte-form/tests/large-components/rune.ts b/packages/svelte-form/tests/large-components/rune.ts
new file mode 100644
index 000000000..2b96a7346
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/rune.ts
@@ -0,0 +1,9 @@
+import { createFormCreator } from '../../src/createFormCreator.svelte.js'
+import TextField from './text-field.svelte'
+
+export const { createAppForm } = createFormCreator({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {},
+})
diff --git a/packages/svelte-form/tests/large-components/text-field.svelte b/packages/svelte-form/tests/large-components/text-field.svelte
new file mode 100644
index 000000000..267569203
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/text-field.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {label}
+ field.handleChange((e.target as any).value)}
+ />
+
diff --git a/packages/svelte-form/tests/large.svelte b/packages/svelte-form/tests/large.svelte
new file mode 100644
index 000000000..30ee0f575
--- /dev/null
+++ b/packages/svelte-form/tests/large.svelte
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+
+{JSON.stringify(formState.current, null, 2)}
diff --git a/packages/svelte-form/tests/large.test.ts b/packages/svelte-form/tests/large.test.ts
new file mode 100644
index 000000000..9ccdc0a0c
--- /dev/null
+++ b/packages/svelte-form/tests/large.test.ts
@@ -0,0 +1,38 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { userEvent } from '@testing-library/user-event'
+import { mount, unmount } from 'svelte'
+import TestForm, { getSampleData } from './large.svelte'
+
+describe('Svelte Tests', () => {
+ let element: HTMLDivElement
+ let instance: any
+ beforeEach(async () => {
+ element = document.createElement('div')
+ document.body.appendChild(element)
+ instance = mount(TestForm, {
+ target: element,
+ })
+ })
+
+ afterEach(() => {
+ unmount(instance)
+ element.remove()
+ })
+
+ it('should have initial values', async () => {
+ expect(element.querySelector('#firstName')).toHaveValue(
+ getSampleData().firstName,
+ )
+ })
+
+ it('should mirror user input', async () => {
+ const firstName = element.querySelector('#firstName')!
+ const firstNameValue = 'Jobs'
+ await userEvent.type(firstName, firstNameValue)
+
+ const form = JSON.parse(element.querySelector('pre')!.textContent!)
+ expect(form.values.firstName).toBe(
+ getSampleData().firstName + firstNameValue,
+ )
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1629f5386..52748070a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1014,6 +1014,28 @@ importers:
specifier: ^7.2.2
version: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)
+ examples/svelte/large-form:
+ dependencies:
+ '@tanstack/svelte-form':
+ specifier: ^1.23.0
+ version: link:../../../packages/svelte-form
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^5.1.1
+ version: 5.1.1(svelte@5.41.1)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))
+ '@tsconfig/svelte':
+ specifier: ^5.0.5
+ version: 5.0.5
+ svelte:
+ specifier: ^5.39.4
+ version: 5.41.1
+ typescript:
+ specifier: 5.8.2
+ version: 5.8.2
+ vite:
+ specifier: ^7.2.2
+ version: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)
+
examples/svelte/simple:
dependencies:
'@tanstack/svelte-form':