# Formisch > The lightweight, schema-first and fully type-safe form library for Angular, Preact, Qwik, React, React Native, Solid, Svelte and Vue. ## Get started (guides) ### Introduction Formisch is a schema-based, headless form library for Svelte. It manages form state and validation. It is type-safe, fast by default and its bundle size is small due to its modular design. Try it out in our [playground](/playground/login/)! #### Highlights - Small bundle size starting at 2.5 kB - Schema-based validation with Valibot - Type safety with autocompletion in editor - Open source and fully tested with 100% coverage - It's fast – DOM updates are fine-grained - Minimal, readable and well thought out API - Supports all native HTML form fields #### Example Every form starts with the [`createForm`](/svelte/api/createForm.md) rune. It initializes your form's store based on the provided Valibot schema and infers its types. Next, wrap your form in the [`
`](/svelte/api/Form.md) component. It's a thin layer around the native `` element that handles form validation and submission. Then, you can access the state of a field with the [`useField`](/svelte/api/useField.md) rune or the [``](/svelte/api/Field.md) component to connect your inputs. ```svelte console.log(output)}> {#snippet children(field)}
{#if field.errors}
{field.errors[0]}
{/if}
{/snippet}
{#snippet children(field)}
{#if field.errors}
{field.errors[0]}
{/if}
{/snippet}
``` In addition, Formisch offers several functions (we call them "methods") that can be used to read and manipulate the form state. These include [`focus`](/methods/api/focus.md), [`getDeepErrorEntries`](/methods/api/getDeepErrorEntries.md), [`getDeepErrors`](/methods/api/getDeepErrors.md), [`getDirtyInput`](/methods/api/getDirtyInput.md), [`getDirtyPaths`](/methods/api/getDirtyPaths.md), [`getErrors`](/methods/api/getErrors.md), [`getInput`](/methods/api/getInput.md), [`handleSubmit`](/methods/api/handleSubmit.md), [`insert`](/methods/api/insert.md), [`isDirty`](/methods/api/isDirty.md), [`isEdited`](/methods/api/isEdited.md), [`isTouched`](/methods/api/isTouched.md), [`isValid`](/methods/api/isValid.md), [`move`](/methods/api/move.md), [`pickDirty`](/methods/api/pickDirty.md), [`remove`](/methods/api/remove.md), [`replace`](/methods/api/replace.md), [`reset`](/methods/api/reset.md), [`setErrors`](/methods/api/setErrors.md), [`setInput`](/methods/api/setInput.md), [`submit`](/methods/api/submit.md), [`swap`](/methods/api/swap.md) and [`validate`](/methods/api/validate.md). These methods allow you to control the form programmatically. #### Comparison What makes Formisch unique is its framework-agnostic core, which is fully native to the framework you are using. It works by inserting framework-specific reactivity blocks when the core package is built, giving you native performance for any UI update. A modular methods API keeps bundles starting at just ~2.5 kB by only including the methods you import, and end-to-end type safety covers deeply nested paths and field arrays with TypeScript inference that stays fast even as forms grow. For a side-by-side look at how Formisch compares to Superforms and TanStack Form, see the [comparison guide](/svelte/guides/comparison.md). #### Vision My vision for Formisch is to create a framework-agnostic platform similar to [Vite](https://vite.dev/), but for forms — a shared core that lets the same mental model and codebase work natively across every modern UI framework. #### Feedback Find a bug or have an idea how to improve the library? Please fill out an [issue](https://github.com/open-circle/formisch/issues/new). Together we can make forms even better! #### License This project is available free of charge and licensed under the [MIT license](https://github.com/open-circle/formisch/blob/main/LICENSE.md). ### Installation Below you will learn how to add Formisch to your project. #### TypeScript If you are using TypeScript, we recommend that you enable strict mode in your `tsconfig.json` so that all types are calculated correctly. > The minimum required TypeScript version is v5.0.2. ```jsonc { "compilerOptions": { "strict": true, // ... }, } ``` #### Install Valibot Formisch uses [Valibot](https://valibot.dev/) for schema-based validation. You need to install it first because it is a peer dependency. ```bash npm install valibot # npm yarn add valibot # yarn pnpm add valibot # pnpm bun add valibot # bun deno add npm:valibot # deno ``` #### Install Formisch You can add Formisch to your project with a single command using your favorite package manager. ```bash npm install @formisch/svelte # npm yarn add @formisch/svelte # yarn pnpm add @formisch/svelte # pnpm bun add @formisch/svelte # bun deno add npm:@formisch/svelte # deno ``` Then you can import it into any JavaScript or TypeScript file. ```ts import { … } from '@formisch/svelte'; ``` #### For AI Agents We provide agent skills that teach AI agents the correct patterns for working with Valibot and Formisch. You can install them by running the following command in your terminal: ```bash npx skills add open-circle/agent-skills --skill formisch valibot ``` You can learn more about the Valibot and Formisch agent skill [here](https://github.com/open-circle/agent-skills). ### Coding agents Formisch is built to be consumed by AI coding agents. This page describes the agent skill, MCP server and Markdown documentation we provide so that your AI tools generate correct Svelte code with Formisch. #### Agent skill Formisch defines form values and validation with [Valibot](https://valibot.dev/) schemas, so an agent needs two skills: our [Formisch SKILL.md](https://github.com/open-circle/agent-skills/blob/main/skills/formisch/SKILL.md) for building forms and managing form state, and the [Valibot SKILL.md](https://github.com/open-circle/agent-skills/blob/main/skills/valibot/SKILL.md) for writing the schemas. Install both with the [Agent Skills CLI](https://agentskills.io/): ```bash npx skills add open-circle/agent-skills --skill formisch valibot ``` The Formisch skill is also published at [`/.well-known/agent-skills/formisch/SKILL.md`](/.well-known/agent-skills/formisch/SKILL.md) and listed in our [discovery index](/.well-known/agent-skills/index.json). For Valibot's MCP server and llms.txt files, see [Valibot's coding agents guide](https://valibot.dev/guides/coding-agents/). #### MCP server We host a [Model Context Protocol](https://modelcontextprotocol.io/) server at `https://formisch.dev/mcp` that gives coding agents first-class tools to search and read the documentation instead of crawling pages. It is free and requires no authentication. The server provides three tools: - `search_docs` searches the documentation and returns the most relevant pages - `get_doc` reads a documentation page or blog post as Markdown - `list_docs` lists all pages grouped by framework and area Add it to Claude Code with: ```bash claude mcp add --transport http formisch https://formisch.dev/mcp ``` Or add it to the MCP configuration of your tool: ```jsonc { "mcpServers": { "formisch": { "url": "https://formisch.dev/mcp", }, }, } ``` The server metadata is available at [`/.well-known/mcp/server-card.json`](/.well-known/mcp/server-card.json). #### LLMs.txt We provide several [LLMs.txt](https://llmstxt.org/) files. Use the one that works best with your AI tool. - [`llms.txt`](/llms.txt) contains a table of contents with links to all Markdown files - [`llms-svelte.txt`](/llms-svelte.txt) contains a table of contents with links to Svelte-related files - [`llms-svelte-full.txt`](/llms-svelte-full.txt) contains the Markdown content of the entire Svelte docs - [`llms-svelte-guides.txt`](/llms-svelte-guides.txt) contains the Markdown content of the Svelte guides - [`llms-svelte-api.txt`](/llms-svelte-api.txt) contains the Markdown content of the Svelte API reference - [`llms-blog.txt`](/llms-blog.txt) contains the Markdown content of all blog posts #### Markdown for agents Every documentation page is available as Markdown. Append `.md` to the URL of a page (e.g. `/svelte/guides/installation/` becomes [`/svelte/guides/installation.md`](/svelte/guides/installation.md)) or request the page with an `Accept: text/markdown` header to receive its Markdown version. #### Custom building blocks Coding agents can use `@formisch/svelte/internals` to build custom methods and framework bindings with the same core helpers as Formisch. See the [internals guide](/svelte/guides/internals.md) for a typed example and guidance on reactivity and lifecycle behavior. ### Comparison Formisch is one of several form libraries available for Svelte. The two most common alternatives are [Superforms](https://superforms.rocks) and [TanStack Form](https://tanstack.com/form/latest). This page is meant as a quick reference for picking the right tool. #### At a glance | | **Formisch** | Superforms | TanStack Form | | ---------------------- | -------------------------------------------------------------- | ------------------------------------ | --------------------------------------- | | Type source | Inferred from schema | Inferred from schema | Inferred from `defaultValues` | | Validation location | Defined in schema | Defined in schema | Per-validator config | | Validation timing | Form-wide `validate` / `revalidate` | Configurable per form / event | Per-validator trigger | | Async validation | Built-in via schema | Built-in (client + server) | Built-in `isValidating` | | Reactivity scope | Fine-grained per rune | Per Svelte store / rune | Per subscription | | Schema libraries | Valibot | Zod, Valibot, Yup, ArkType, … | Standard Schema | | Server integration | Client-only (SvelteKit support planned) | First-class (SvelteKit form actions) | Client-only | | Bundle size (min+gzip) | From ~2.5 kB | ~20 kB | ~15 kB | | Framework support | Angular, React, React Native, Preact, Solid, Svelte, Vue, Qwik | Svelte / SvelteKit | React, Vue, Solid, Svelte, Lit, Angular | The table is intentionally short. It only covers the dimensions that most often drive a library choice in practice. Other differences such as devtools, ecosystem maturity, and community size are real but tend to matter less than how each library handles types, validation, and reactivity. #### Why Formisch? Three reasons to pick Formisch over the alternatives above: **One schema, no second source of truth.** A single Valibot schema is everything the form needs: the runtime validator, the source of types, and the description of the form's structure — all at once. There is no separate TypeScript generic to declare, no `defaultValues` object to keep aligned with the schema, no resolver to configure. When the schema changes, every part of the form follows — at compile time and at runtime. **The smallest bundle, by a wide margin.** Formisch starts at ~2.5 kB and grows only as you import additional methods like `focus`, `getInput`, and `reset`. That is several times smaller than the alternatives in the table above — and it stays that way because the core is intentionally small and the library is fully tree-shakeable, so methods you don't import don't end up in your bundle. **Type safety that stays fast.** Types flow from the schema through every API, including deeply nested paths and field arrays. The inference is structured to keep TypeScript editor performance from degrading as schemas grow — which matters in large codebases where heavily-generic form libraries become a friction point. #### Which library should you use? **Superforms** is the strongest choice when forms are tightly coupled to SvelteKit's server form actions. It supports unified client and server validation, progressive enhancement, and a wide range of schema libraries through Standard Schema. If SvelteKit is doing the heavy lifting on the server, Superforms fits naturally. **TanStack Form** is a good fit when you need fine-grained control over validation timing and built-in async validation handling without building that infrastructure yourself. It is also the natural choice if your team is already invested in the TanStack ecosystem and values a consistent mental model across data fetching, routing, and forms. **Formisch** makes the most sense for new projects in TypeScript-heavy codebases, especially when you expect forms to grow in complexity. The schema-first design means there is a single source of truth for types, runtime validation, and form structure, so there is less to keep aligned over time. Reactivity is fine-grained through Svelte 5 runes. The main considerations are that Formisch currently supports only [Valibot](https://valibot.dev) as the schema library, and is presently a client-side form layer — dedicated SvelteKit integration is planned for one of the next releases. #### Migrating to Formisch Migrating to Formisch is not a drop-in replacement. Superforms in particular is built around SvelteKit's server form actions, while Formisch is a pure client-side form layer, so migration typically means moving validation and submission out of `+page.server.ts` actions into client-side handlers. The libraries can coexist in the same application, so you can migrate one form at a time. We provide a dedicated migration guide for each library, with a side-by-side example, step-by-step instructions, and an API mapping table: [migrate from Superforms](/svelte/guides/migrate-from-superforms.md) and [migrate from TanStack Form](/svelte/guides/migrate-from-tanstack-form.md). #### Next steps If you have decided that Formisch is a good fit, install it via the [installation](/svelte/guides/installation.md) guide and start building by [defining your form](/svelte/guides/define-your-form.md). ## Main concepts (guides) ### Define your form Creating a form in Formisch starts with defining a Valibot schema. The schema serves as the blueprint for your form, outlining the structure, data types, and validation rules for each field. #### Schema definition Formisch is a schema-first form library built on top of [Valibot](https://valibot.dev/). When you create a form with [`createForm`](/svelte/api/createForm.md), TypeScript types are automatically inferred from your schema, giving you full autocompletion and type safety throughout your form without needing to write any manual type definitions. ##### Example schema The following schema defines a form with two required string fields. The `email` field must be a valid email format, and the `password` field must be at least 8 characters long. Each validation includes custom error messages that will be displayed when validation fails. > For more complex schema examples, check out the schemas of our [playground](/playground/login/). ```ts import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe( v.string(), v.nonEmpty('Please enter your email.'), v.email('The email address is badly formatted.') ), password: v.pipe( v.string(), v.nonEmpty('Please enter your password.'), v.minLength(8, 'Your password must have 8 characters or more.') ), }); ``` ##### Field order The order in which you define fields in your schema matters. When a form is submitted with invalid values, Formisch focuses the first field with an error. Since this follows your schema, define your fields in the same order they appear in your form, so the focus moves to the first invalid field instead of one further down the page. #### Schema validation Your schema definition should reflect exactly the data you expect when submitting the form. For example, if the value of a field is optional and will only be submitted in certain cases, your schema should reflect this information by using `v.optional(…)`. ```ts import * as v from 'valibot'; const ProfileSchema = v.object({ name: v.pipe(v.string(), v.nonEmpty()), bio: v.optional(v.string()), // <- Optional field }); ``` Formisch validates your form values against the schema before submission, ensuring that your form can only be submitted if it matches your schema definition. #### Next steps Now that you understand how to define your form schema, continue to the [create your form](/svelte/guides/create-your-form.md) guide to learn how to initialize your form with [`createForm`](/svelte/api/createForm.md). ### Create your form Formisch consists of runes, components and methods. To create a form you use the [`createForm`](/svelte/api/createForm.md) rune. #### Form rune The [`createForm`](/svelte/api/createForm.md) rune initializes and returns the store of your form. The store contains the state of the form and can be used with other Formisch runes, components and methods to build your form. ```svelte
console.log(output)}>
``` ##### Configuration options The [`createForm`](/svelte/api/createForm.md) rune accepts a configuration object with the following options: - `schema`: Your Valibot schema that defines the form - `initialInput`: Initial values for your form fields (optional) - `emptyInput`: The empty value each field type starts at when a required field has no initial input (optional, defaults to `{ string: '' }`) - `validate`: When validation first occurs (optional, defaults to `'submit'`) - `revalidate`: When revalidation occurs after initial validation (optional, defaults to `'input'`) ```ts const loginForm = createForm({ schema: LoginSchema, initialInput: { email: 'user@example.com', }, validate: 'initial', revalidate: 'input', }); ``` Formisch tracks two inputs for every field: the **initial input** (baseline for dirty tracking) and the **current input** (what the user is editing). In many apps, the initial input represents the server state while the current input represents the client state. `isDirty` becomes `true` when a field's current input differs from its initial input. Use [`setInput`](/methods/api/setInput.md) to update the current input (client state), and use [`reset`](/methods/api/reset.md) to update the initial input (baseline) when your server data changes or is refreshed. ##### Empty input By default, a required string field starts as an empty string (`''`) instead of `undefined`, matching an empty text input. This way an empty field shows the validation message you defined for it (for example from `v.nonEmpty()`) without you setting an `initialInput` for every field. You can configure the empty value per field type, or opt out by setting a type to `undefined`: ```ts const loginForm = createForm({ schema: LoginSchema, emptyInput: { string: '', // the default number: 0, // required numbers start at 0 instead of undefined boolean: false, // checkboxes start unchecked }, }); ``` Optional and nullable fields are never affected and keep starting as `undefined`, since they accept it. The supported types are `string`, `number`, `boolean` and `date`, and the default is `{ string: '' }`. #### Multiple forms When a page contains multiple forms, you can create separate form stores for each one: ```svelte
console.log(output)}>
console.log(output)}>
``` If you need a multi-step form (wizard), see the [multi-step form discussion](https://github.com/open-circle/formisch/discussions/108) for patterns and approaches. #### Next steps Now that you know how to create a form, continue to the [add form fields](/svelte/guides/add-form-fields.md) guide to learn how to connect your input elements to the form using the [`Field`](/svelte/api/Field.md) component. ### Add form fields To add a field to your form, you can use the [`Field`](/svelte/api/Field.md) component or the [`useField`](/svelte/api/useField.md) rune. Both are headless and provide access to field state for building your form UI. #### Field component The [`Field`](/svelte/api/Field.md) component has two mandatory properties: `of` which accepts the form store, and `path` which specifies which field to connect. If you use TypeScript, you get full autocompletion for the path based on your schema. ##### Snippet syntax Use Svelte's `{#snippet children(field)}` syntax to define how the field should render. The field store parameter includes the current value, error messages, and props to spread onto your input element. ```svelte
console.log(output)}> {#snippet children(field)} {/snippet} {#snippet children(field)} {/snippet}
``` > **Important:** The event handlers in `field.props` read the value from the DOM as a string. Fields with a non-string schema like `v.number()` or `v.date()` therefore need to be controlled to store the value with the correct type. See the [controlled fields](/svelte/guides/controlled-fields.md) guide to learn more. > **Important:** If you plan to set initial values with `initialInput` or programmatically control field values using methods like [`setInput`](/methods/api/setInput.md) or [`reset`](/methods/api/reset.md), you must make your fields controlled by setting the appropriate attributes (like `value`, `checked`, or `selected`). See the [controlled fields](/svelte/guides/controlled-fields.md) guide to learn more. ##### Headless design The [`Field`](/svelte/api/Field.md) component does not render its own UI elements. It is headless and provides only the data layer of the field. This allows you to freely define your user interface. You can use HTML elements, custom components or an external UI library. ##### Path array The `path` property accepts an array of strings and numbers that represents the path to the field in your schema. For top-level fields, it's simply the field name wrapped in an array: ```svelte {#snippet children(field)} {/snippet} ``` For nested fields, the path reflects the structure of your schema: ```svelte {#snippet children(field)} {/snippet} ``` ##### Type safety The API design of the [`Field`](/svelte/api/Field.md) component results in a fully type-safe form. For example, if you change your schema, TypeScript will immediately alert you if the path is invalid. The field state is also fully typed based on your schema, giving you autocompletion for properties like `field.input`. #### useField rune For very complex forms where you create individual components for each form field, Formisch provides the [`useField`](/svelte/api/useField.md) rune. It allows you to access the field state directly within your component logic. ```svelte
{#if field.errors}
{field.errors[0]}
{/if}
``` ##### When to use which - **Use [`Field`](/svelte/api/Field.md) component**: When defining multiple fields in the same component. It ensures you don't accidentally access the wrong field store. - **Use [`useField`](/svelte/api/useField.md) rune**: When creating field components for single fields. It allows you to access field state in your component logic. The [`Field`](/svelte/api/Field.md) component is essentially a thin wrapper around [`useField`](/svelte/api/useField.md) that allows you to access the field state within your snippet code. #### Field store The field store provides access to the following properties: - `path`: The path to the field within the form. - `props`: Props to spread onto your input element (includes event handlers, name attribute, and `autofocus` to automatically focus fields with errors). - `input`: The current input value of the field. - `errors`: An array of error messages if validation fails. - `isTouched`: Whether the field has been touched. - `isEdited`: Whether the field's value has been changed. - `isDirty`: Whether the current input differs from the initial input. - `isValid`: Whether the field passes all validation rules. - `onInput`: Sets the field input value programmatically. Use this when the field cannot be connected to a native HTML element (e.g., complex custom inputs or component libraries that don't expose the underlying element). #### Next steps Now that you know how to add fields to your form, continue to the [input components](/svelte/guides/input-components.md) guide to learn about creating reusable input components for your forms. ### Input components To make your code more readable, we recommend that you develop your own input components if you are not using a prebuilt UI library. There you can encapsulate logic to display error messages, for example. > If you're already a bit more experienced, you can use the input components we developed for our [playground](/playground/login/) as a starting point. You can find the code in our GitHub repository [here](https://github.com/open-circle/formisch/tree/main/playgrounds/svelte/src/lib/components). #### Why input components? Currently, your fields might look something like this: ```svelte {#snippet children(field)}
{#if field.errors}
{field.errors[0]}
{/if}
{/snippet}
``` If CSS and a few more functionalities are added here, the code quickly becomes confusing. In addition, you have to rewrite the same code for almost every form field. Our goal is to develop a `TextInput` component so that the code ends up looking like this: ```svelte {#snippet children(field)} {/snippet} ``` #### Create an input component In the first step, you create a new file for the `TextInput` component and, if you use TypeScript, define its properties. ```svelte ``` ##### Component markup In the next step, add the component markup to display the input and error messages. ```svelte
{#if label} {/if} {#if errors}
{errors[0]}
{/if}
``` ##### Next steps You can now build on this code and add CSS, for example. You can also follow the procedure to create other components such as `Checkbox`, `Slider`, `Select` and `FileInput`. ##### Final code Below is an overview of the entire code of the `TextInput` component. ```svelte
{#if label} {/if} {#if errors}
{errors[0]}
{/if}
``` #### Using component libraries When using component libraries that don't expose their underlying native HTML elements, you cannot spread `field.props` directly. Instead, use `field.onInput` to update the value programmatically: ```svelte {#snippet children(field)} field.onInput(newDate)} /> {/snippet} ``` The `field.onInput` method updates the field value and triggers validation. For more details, see the [controlled fields](/svelte/guides/controlled-fields.md) guide. For ready-made wiring recipes, check out our integration guides: - [shadcn-svelte](/svelte/guides/shadcn-svelte.md) - [Ark UI](/svelte/guides/ark-ui.md) - [Bits UI](/svelte/guides/bits-ui.md) #### Next steps Now that you know how to create reusable input components, continue to the [handle submission](/svelte/guides/handle-submission.md) guide to learn how to process form data when the user submits the form. ### Handle submission Your first form is almost ready. The remaining step is processing its data when the form is submitted. #### Submit event To process the values on submission, you need to pass a function to the `onsubmit` property of the [`Form`](/svelte/api/Form.md) component. The first parameter passed to the function contains the validated form values. ```svelte
``` The [`SubmitHandler`](/core/api/SubmitHandler.md) type ensures type safety for your submission handler, automatically inferring the types of validated values from your schema. If you need access to the submit event, use [`SubmitEventHandler`](/core/api/SubmitEventHandler.md) instead. ##### Prevent default When the form is submitted, `event.preventDefault()` is executed by default to prevent the window from reloading so that the values can be processed directly in the browser and the state of the form is preserved. ##### Loading state While the form is being submitted, you can use `loginForm.isSubmitting` to display a loading animation and disable the submit button: ```svelte ``` The form store also provides other reactive properties like `isSubmitted`, `isValidating`, `isTouched`, `isDirty`, `isValid`, and `errors` for tracking form state. Note that `errors` only contains validation errors at the root level of the form — to get all errors from all fields, use the [`getDeepErrors`](/methods/api/getDeepErrors.md) method. ##### Async submission The submit handler can be asynchronous, allowing you to perform API calls or other async operations: ```svelte ``` ##### Trigger submission If you want to trigger submission programmatically from outside the form, you can use the [`submit`](/methods/api/submit.md) method. It calls `requestSubmit()` on the underlying form element: ```svelte ``` #### Submit without \
In some cases, you may not be able to wrap your fields in a `` element — for example, when your form is rendered inside another form, since nesting `` elements is invalid HTML. In these situations, you can use the [`handleSubmit`](/methods/api/handleSubmit.md) method directly to submit the form programmatically without the [`Form`](/svelte/api/Form.md) component. The returned function accepts no arguments and can be called from anywhere — for example, from a button's `onclick` handler: ```svelte
``` #### Next steps Congratulations! You've learned the core concepts of building forms with Formisch. To learn more about advanced features, check out the [form methods](/svelte/guides/form-methods.md) guide to discover how to programmatically control your forms. ### Form methods To retrieve the values of your form or to make changes to the form, Formisch provides you with several methods. These apply either to the entire form or to individual fields. #### Reading values To retrieve values from your form, you can use: - [`getInput`](/methods/api/getInput.md): Get the current value of a specific field - [`getErrors`](/methods/api/getErrors.md): Get error messages for a specific field - [`getDeepError`](/methods/api/getDeepError.md): Get the first error message within a field or the form - [`getDeepErrors`](/methods/api/getDeepErrors.md): Get all error messages across the entire form - [`getDeepErrorEntry`](/methods/api/getDeepErrorEntry.md): Get the error messages of the first erroring field paired with its path - [`getDeepErrorEntries`](/methods/api/getDeepErrorEntries.md): Get all error messages paired with the path of the field they belong to Formisch uses reactive stores internally, which means that reading values with these methods is reactive. When the form state changes, any component or subscription that uses these methods will automatically update to reflect the new state. #### Field state To check the state of a single field or of the entire form, use: - [`isTouched`](/methods/api/isTouched.md): Whether the field or form has been touched - [`isEdited`](/methods/api/isEdited.md): Whether the field or form has been edited - [`isDirty`](/methods/api/isDirty.md): Whether the field or form differs from its initial input - [`isValid`](/methods/api/isValid.md): Whether the field or form has no validation errors Each of these takes an optional `path` to scope the check to a single field. Without it, they walk the entire form. #### Dirty state To work with the dirty state of your form, Formisch provides three methods: - [`getDirtyInput`](/methods/api/getDirtyInput.md): Get the dirty parts of the form input - [`getDirtyPaths`](/methods/api/getDirtyPaths.md): Get the paths of dirty fields - [`pickDirty`](/methods/api/pickDirty.md): Filter an externally-supplied value down to its dirty parts using the form's dirty mask See the [dirty fields](/svelte/guides/dirty-fields.md) guide for when to reach for each. #### Setting values To manually update form values or errors, use: - [`setInput`](/methods/api/setInput.md): Manually update the value of a specific field - [`setErrors`](/methods/api/setErrors.md): Manually set error messages for a specific field > When you have access to a field store (from [`useField`](/svelte/api/useField.md) or [`Field`](/svelte/api/Field.md)), you can use `field.onInput(value)` to update the value directly. This is especially useful for custom inputs or component libraries that don't expose native HTML elements. > If you need to update the form because the initial data has changed (e.g., remote data was refreshed), use [`reset`](/methods/api/reset.md) with a new `initialInput` instead of [`setInput`](/methods/api/setInput.md). The [`reset`](/methods/api/reset.md) method properly reinitializes the form state, while [`setInput`](/methods/api/setInput.md) only changes the current input values without updating the initial state. #### Form control To control the form programmatically, use: - [`handleSubmit`](/methods/api/handleSubmit.md): Create a submit event handler that validates the form and calls your handler on success - [`reset`](/methods/api/reset.md): Reset the form to its initial state or update initial values - [`validate`](/methods/api/validate.md): Manually trigger validation of the entire form - [`submit`](/methods/api/submit.md): Programmatically trigger form submission - [`focus`](/methods/api/focus.md): Focus on a specific field #### Array operations For working with field arrays, Formisch provides: - [`insert`](/methods/api/insert.md): Insert a new item into a field array - [`remove`](/methods/api/remove.md): Remove an item from a field array - [`move`](/methods/api/move.md): Move an item to a different position in a field array - [`swap`](/methods/api/swap.md): Swap two items in a field array - [`replace`](/methods/api/replace.md): Replace an item in a field array #### API design All methods in Formisch follow a consistent API pattern: the first parameter is always the form store, and the second parameter (if necessary) is always a config object. This design makes the API flexible and consistent across all methods. Once you understand this pattern, you basically understand the entire API design. Here are some examples: ```ts // Get the value of a field const emailInput = getInput(loginForm, { path: ['email'] }); // Reset the form with new initial values reset(loginForm, { initialInput: { email: '', password: '' } }); // Move an item in a field array move(loginForm, { path: ['items'], from: 0, to: 3 }); ``` #### API reference You can find detailed documentation for each method in our [API reference](/svelte/api/). ## Advanced guides (guides) ### Validation Formisch validates your form against your Valibot schema — the same one that defines its types. This guide explains how validation runs under the hood, how the `validate` and `revalidate` config control its timing, how to display errors only when they are helpful, and how to react to asynchronous validation with `isValidating`. #### How validation works Whenever validation is triggered, Formisch parses the **entire form** against your schema in a single pass. It then distributes the resulting issues to the individual fields: every field with an issue receives its error messages, and every field without one is cleared. A field is valid when it has no errors. > Validation always runs against the whole schema, even when a single field triggers it. This keeps cross-field rules (like a `password` and `confirmPassword` that must match) correct without any extra wiring. The result is still stored per field, so each `` only re-renders when its own errors change. Because Formisch parses with Valibot's asynchronous API, schemas with async checks (for example, a server-side uniqueness check) work out of the box. While such a check is in flight, the form's `isValidating` state is `true` (see [below](#validating-state)). #### When validation runs Two config options control the timing of validation: - `validate`: when a field is validated for the **first** time. Defaults to `'submit'`. - `revalidate`: when a field is validated **again**, once it already has an error or the form has been submitted. Defaults to `'input'`. Both accept the following modes: - `'initial'`: immediately, when the form is created (only valid for `validate`, not `revalidate`). - `'touch'`: when the field is first focused. - `'input'`: on every keystroke. - `'change'`: on the field's change event. - `'blur'`: when the field loses focus. - `'submit'`: only when the form is submitted. ```ts const loginForm = createForm({ schema: LoginSchema, validate: 'blur', revalidate: 'input', }); ``` The split between `validate` and `revalidate` is what makes good defaults possible. With the default `validate: 'submit'` and `revalidate: 'input'`, a field stays quiet until the user submits, and only then does it start correcting itself live as the user fixes it. The configuration above (`validate: 'blur'`) is a gentler variant: a field is first checked when the user leaves it, then re-checked on every keystroke once it has an error. #### Showing errors at the right time `validate` and `revalidate` control **when validation runs** — but not **when errors are shown**. After a validation pass, every invalid field has its `errors` populated, and it is up to you to decide which ones to render. Showing every error the moment it appears can overwhelm the user, while showing them only after submit forces them to scroll back and fix fields they have already filled out. A balanced approach is to reveal a field's error once the user has actually changed it, and to reveal all remaining errors after the first submit attempt. Each field exposes several state flags to drive this decision: - `isTouched`: the field has been focused or changed. - `isEdited`: the field's value has been changed (but not merely focused), and stays `true` even if the value is changed back to its initial value. - `isDirty`: the field's current value differs from its initial value. The form additionally exposes `isSubmitted`, which becomes `true` after the first submit attempt. Combining `isEdited` with `isSubmitted` gives the balanced behavior described above: ```svelte console.log(output)}> {#snippet children(field)} {#if (field.isEdited || loginForm.isSubmitted) && field.errors}
{field.errors[0]}
{/if} {/snippet}
``` `isEdited` is the right flag for this. `isTouched` would reveal the error as soon as the user tabs through a field without changing it, and `isDirty` would hide the error again if the user clears an invalid value back to its initial state. `isEdited` avoids both: it turns on only after a real change and stays on. Pair it with `validate: 'input'` (or `'change'`) so the error appears as soon as the field becomes invalid, and with `isSubmitted` so that the errors of all fields — including the ones the user never touched — become visible after a submit attempt. ##### Reuse the guard across fields Writing this guard at every field gets repetitive. The common fix is to move it into a small reusable component, either on its own or folded into the `TextInput` from the [input components](/svelte/guides/input-components.md) guide, so the `isEdited || isSubmitted` logic lives in exactly one place: ```svelte {#if errors && (isEdited || isSubmitted)}
{errors[0]}
{/if} ``` Each field then stays focused on its input: ```svelte {#snippet children(field)} {/snippet} ``` #### Validating state When your schema contains asynchronous checks, validation does not resolve instantly. The form's `isValidating` state is `true` while any validation is in flight, which you can use to show a loading indicator or to disable the submit button until validation settles. ```svelte
console.log(output)}>
``` `isValidating` and `isSubmitting` overlap but are not the same. `isValidating` is `true` whenever validation runs — triggered by a field event or as part of a submit. `isSubmitting` is `true` for the entire submission, which covers both validating the form and running your submit handler. So during a submit both are `true` while the form validates, and once validation passes only `isSubmitting` stays `true` while your handler runs. #### Further reading To process your form's values once they pass validation, see the [handle submission](/svelte/guides/handle-submission.md) guide. For working with the fields a user has changed, see the [dirty fields](/svelte/guides/dirty-fields.md) guide. ### Special inputs As listed in our features, the library supports all native HTML form fields. This includes the HTML `` element. > In our [playground](/playground/special/) you can take a look at such fields and test them out. #### Checkbox A simple checkbox represents a boolean and is `true` when checked or `false` otherwise. ```svelte {#snippet children(field)} {/snippet} ``` However, you can also use multiple checkboxes to represent an array of strings. For this you simply have to add the `value` attribute to the HTML `` element. ```svelte {#each [{ label: 'Bananas', value: 'bananas' }, { label: 'Apples', value: 'apples' }, { label: 'Grapes', value: 'grapes' }] as { label, value }} {#snippet children(field)} {/snippet} {/each} ``` #### Radio A group of radio buttons, while similar to an array of checkboxes, will only allow you to select one of the options based on which button is checked. ```svelte {#snippet children(field)} {#each [{ label: 'Red', value: 'red' }, { label: 'Green', value: 'green' }, { label: 'Blue', value: 'blue' }] as { label, value }} {/each} {/snippet} ``` #### Select An HTML ` {#each [{ label: 'Preact', value: 'preact' }, { label: 'Solid', value: 'solid' }, { label: 'Qwik', value: 'qwik' }] as { label, value }} {/each} {/snippet}
``` However, if you set the `multiple` attribute, multiple options can be selected making the field represent an array of strings. ```svelte {#snippet children(field)} {/snippet} ``` #### File An HTML `` works similarly to an HTML ` {/snippet} ``` With the `multiple` attribute, users can select multiple files: ```svelte {#snippet children(field)} {/snippet} ``` ### Controlled fields By default, all form fields are uncontrolled because that's the default behavior of the browser. For a simple login or contact form this is quite sufficient. #### Why controlled? As soon as your forms become more complex, for example you set initial values or change the values of a form field via [`setInput`](/methods/api/setInput.md), it becomes necessary that you control your fields yourself. For example, depending on which HTML form field you use, you may need to set the `value`, `checked` or `selected` attributes. ##### Text input example For a text input field you simply add the `value` attribute and pass the value of the field: ```svelte {#snippet children(field)} {/snippet} ``` ##### Exception for files The HTML `` element is an exception because it cannot be controlled. However, you have the possibility to control the UI around it. For inspiration you can use the code of our [`FileInput`](https://github.com/open-circle/formisch/blob/main/playgrounds/svelte/src/lib/components/FileInput.svelte) component from our [playground](/playground/special/). #### Numbers and dates Fields defined with a `v.number()` or `v.date()` schema need extra steps to be controlled, because the `` element natively understands only strings as value. ##### Number input example An `` only reads and writes strings. So if you spread `field.props` onto it, `field.input` holds a string like `"123"`, even though it is typed as `number`. To store a real number, override the input handler with one that converts the value via `valueAsNumber` and forwards it to `field.onInput`. You also have to handle `NaN`. While typing a floating point number, the value can briefly be `NaN`, for example right after typing `1.`. If you don't catch this for the displayed value, the input is cleared. It is best to encapsulate both in a separate component as described in the [input components](/svelte/guides/input-components.md) guide. ```svelte
{#if label} {/if} oninput( event.currentTarget.value === '' ? undefined : event.currentTarget.valueAsNumber )} /> {#if errors}
{errors[0]}
{/if}
``` Pass `field.onInput` after spreading `field.props` so it overrides the native handler. ```svelte {#snippet children(field)} {/snippet} ``` ##### Date input example The same applies to dates. An `` works with `yyyy-mm-dd` strings, while you usually want to store a `Date`. So you convert in both directions: format the `Date` for display, and parse the entered string back with `valueAsDate` before storing it. ```svelte
{#if label} {/if} oninput(event.currentTarget.valueAsDate ?? undefined)} /> {#if errors}
{errors[0]}
{/if}
``` As with the number input, pass `field.onInput` after the spread to override the native handler. ```svelte {#snippet children(field)} {/snippet} ``` #### Custom inputs and component libraries Some component libraries don't expose the underlying native HTML element, which means you cannot spread `field.props` onto them. For these cases, use `field.onInput` to set the value programmatically. ```svelte {#snippet children(field)} field.onInput(newDate)} /> {/snippet} ``` This is useful for: - **Component libraries** that wrap native elements without exposing them - **Complex custom inputs** like date pickers, rich text editors, or color pickers The `field.onInput` method updates the field value and triggers validation, just like a native input would. For ready-made wiring recipes, check out our integration guides: - [shadcn-svelte](/svelte/guides/shadcn-svelte.md) - [Ark UI](/svelte/guides/ark-ui.md) - [Bits UI](/svelte/guides/bits-ui.md) #### Next steps Now that you understand controlled fields, you can explore more advanced topics like [nested fields](/svelte/guides/nested-fields.md) and [field arrays](/svelte/guides/field-arrays.md) to handle complex form structures. ### Nested fields To add a little more structure to a complex form or to match the values of the form to your database, you can also nest your form fields in objects as deep as you like. #### Schema definition For example, in the schema below, the first and last name are grouped under the object with the key `name`. ```ts import * as v from 'valibot'; const ContactSchema = v.object({ name: v.object({ first: v.pipe(v.string(), v.nonEmpty()), last: v.pipe(v.string(), v.nonEmpty()), }), email: v.pipe(v.string(), v.email()), message: v.pipe(v.string(), v.nonEmpty()), }); ``` #### Path array When creating a nested field, use an array with multiple elements for the `path` property to refer to the nested field. The array represents the path through the nested structure. ```svelte {#snippet children(field)} {/snippet} ``` If you're using TypeScript, your editor will provide autocompletion for the path based on your schema structure. #### Deep nesting You can nest objects as deeply as needed. Simply extend the path array with additional keys: ```ts const ProfileSchema = v.object({ user: v.object({ address: v.object({ street: v.string(), city: v.string(), country: v.string(), }), }), }); ``` ```svelte {#snippet children(field)} {/snippet} ``` #### Type safety The path array is fully type-safe. TypeScript will validate that each element in your path corresponds to a valid key in your schema structure, providing autocompletion and compile-time errors if you reference a non-existent path. ### Dirty fields When a user edits a form, Formisch tracks which fields have changed since they were initialized. The library exposes three methods for working with that dirty state: [`getDirtyInput`](/methods/api/getDirtyInput.md), [`getDirtyPaths`](/methods/api/getDirtyPaths.md), and [`pickDirty`](/methods/api/pickDirty.md). This guide explains what each one is for, when to reach for it, and why `pickDirty` exists alongside the others. #### What "dirty" means A field is dirty when its current input differs from its start input. The form starts clean. As the user types, only the fields they touch flip to dirty. Resetting a field (or resetting the form with a new `initialInput`) clears the dirty flag. > The dirty flag is per-field. Editing a deeply nested value does not flip its ancestors' flags. The dirty-extraction methods walk the tree internally to find dirty descendants, which is why all three methods do the same kind of work under the hood. #### The three methods ##### `getDirtyInput` Returns the dirty subtree of the form's input. Use it when you want to ship the values the user typed — typically for a PATCH endpoint that accepts the same shape as your form input. ```ts import { createForm, getDirtyInput, type SubmitHandler, } from '@formisch/svelte'; import * as v from 'valibot'; const UserSchema = v.object({ name: v.pipe(v.string(), v.nonEmpty()), email: v.pipe(v.string(), v.email()), }); const { user } = $props(); const profileForm = createForm({ schema: UserSchema, initialInput: user, }); const onSubmit: SubmitHandler = async () => { const dirty = getDirtyInput(profileForm); if (dirty) { await api.patchUser(user.id, dirty); } }; ``` If only `email` was edited, `dirty` is `{ email: 'new@example.com' }`. If nothing was edited, it is `undefined`. Because dirty state is bound to the form input, `getDirtyInput` always returns the raw user input. Schema transformations such as `v.trim()` or `v.toNumber()` are not applied to the returned values. When you need the validated and transformed output instead, reach for `pickDirty` (see [below](#why-pickdirty-exists)). ##### `getDirtyPaths` Returns a list of paths to dirty fields. Use it for logging, telemetry, or driving a custom walker over the form's dirty state. ```ts import { getDirtyPaths } from '@formisch/svelte'; const dirtyPaths = getDirtyPaths(profileForm); // e.g. [['email'], ['user', 'name']] ``` Arrays are atomic: only the array's own path is returned, never the paths of individual items. ##### `pickDirty` Filters a user-supplied value down to its dirty parts using the form's dirty mask. Use it when you have a separately-derived value — most commonly the validated and transformed output from a submit handler — and you want only its dirty parts. ```ts import { pickDirty, type SubmitHandler } from '@formisch/svelte'; const onSubmit: SubmitHandler = async (output) => { const dirty = pickDirty(profileForm, { from: output }); if (dirty) { await api.update(dirty); } }; ``` #### Why `pickDirty` exists Dirty state is tracked against the form **input** — the raw values the user typed. But Valibot schemas can transform that input into a different output shape before it reaches your submit handler: ```ts import * as v from 'valibot'; const Schema = v.object({ name: v.pipe(v.string(), v.trim()), age: v.pipe(v.string(), v.toNumber()), }); ``` After validation, `output.name` has whitespace trimmed and `output.age` is a number. [`getDirtyInput`](/methods/api/getDirtyInput.md) would give you the raw strings, because the dirty state is bound to the form input — not to the validated output. [`pickDirty(form, { from: output })`](/methods/api/pickDirty.md) is the bridge: it walks the form's dirty tree as a structural mask and reads the corresponding values from the output you supply. The result preserves the transformed types. ```ts import { pickDirty, type SubmitHandler } from '@formisch/svelte'; const onSubmit: SubmitHandler = async (output) => { // output.age is a number — pickDirty preserves that. const dirty = pickDirty(form, { from: output }); if (dirty) { await api.update(dirty); } }; ``` If the schema reshapes the output entirely — for example, by combining several input fields into a single output value — `pickDirty` returns `undefined` because the shape no longer aligns with the form's input shape. #### Atomic arrays All three methods treat arrays as atomic. When any descendant of an array is dirty, the full array is returned (or its own path, for `getDirtyPaths`). The methods never produce sparse arrays. Sparse arrays don't round-trip safely. Serializers compact them, `JSON.stringify` writes `null` for holes, and indices lose positional meaning. Returning the full current array preserves order and lets the server treat the array as a complete replacement. This differs from objects, where clean keys are omitted. Objects model keyed dictionaries; arrays model ordered sequences. #### Common patterns The snippets below assume `form` is your form store (the result of [`createForm`](/svelte/api/createForm.md)). ##### Skip submission when nothing changed The form's `isDirty` flag is the cheapest way to ask "is anything dirty?" — it short-circuits on the first dirty field it finds and doesn't allocate. Use it when all you need is the yes/no answer. If you actually consume the output of `getDirtyInput`, `getDirtyPaths`, or `pickDirty`, call that method directly — its return value already signals "nothing dirty", so checking `form.isDirty` first would just walk the tree twice. ```ts import type { SubmitHandler } from '@formisch/svelte'; const onSubmit: SubmitHandler = async (output) => { if (!form.isDirty) { return; } await api.update(output); }; ``` ##### Send only the dirty raw input ```ts import { getDirtyInput } from '@formisch/svelte'; const dirty = getDirtyInput(form); if (dirty) { await api.patch(dirty); } ``` ##### Send only the dirty validated output ```ts import { pickDirty, type SubmitHandler } from '@formisch/svelte'; const onSubmit: SubmitHandler = async (output) => { const dirty = pickDirty(form, { from: output }); if (dirty) { await api.update(dirty); } }; ``` #### Performance All three methods walk the form's field tree and call `getFieldBool` recursively to skip clean subtrees. Cost is effectively linear in field count for typical balanced forms (shallow and wide) and degrades toward `O(N²)` for deeply nested trees with few siblings at each level. Call these methods from submit or blur handlers — not from tight reactive loops on every keystroke. For very large or deeply nested forms (thousands of fields), profile before binding them to high-frequency events. ### Field arrays Dynamically generating form fields from an array introduces additional challenges when items are added, removed, swapped, or moved. Formisch provides the [`FieldArray`](/svelte/api/FieldArray.md) component, which works with its array methods to make these forms straightforward. > In our [playground](/playground/todos/) you can take a look at a form with a field array and test it out. #### Create a field array ##### Schema definition In the following example we create a field array for a todo form with the following schema: ```ts import * as v from 'valibot'; const TodoFormSchema = v.object({ heading: v.pipe(v.string(), v.nonEmpty()), todos: v.pipe( v.array( v.object({ label: v.pipe(v.string(), v.nonEmpty()), deadline: v.pipe(v.string(), v.nonEmpty()), }) ), v.nonEmpty(), v.maxLength(10) ), }); ``` ##### FieldArray component To dynamically generate the form fields for the todos, you use the [`FieldArray`](/svelte/api/FieldArray.md) component in combination with Svelte's `{#each}` block. The field array provides an `items` array that contains unique string identifiers which the `{#each}` block uses to detect when an item is added, moved, or removed. ```svelte
console.log(output)}> {#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
{#snippet children(fieldArray)}
{#each fieldArray.items as item, index (item)}
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
{/each} {#if fieldArray.errors}
{fieldArray.errors[0]}
{/if}
{/snippet}
``` ##### Path array with index As with [nested fields](/svelte/guides/nested-fields.md), you use an array for the `path` property. The key difference is that you include the index from the `{#each}` block's context to specify which array item you're referencing. This ensures the paths update correctly when items are added, moved, or removed. ```svelte {#snippet children(field)} {/snippet} ``` #### Use array methods Now you can use the [`insert`](/methods/api/insert.md), [`move`](/methods/api/move.md), [`remove`](/methods/api/remove.md), [`replace`](/methods/api/replace.md), and [`swap`](/methods/api/swap.md) methods to make changes to the field array. These methods automatically take care of rearranging all the fields. ##### Insert method Add a new item to the array: ```svelte ``` The `at` option can be used to specify the index where the item should be inserted. If not provided, the item is added to the end of the array. ##### Remove method Remove an item from the array: ```svelte ``` ##### Move method Move an item from one position to another: ```svelte ``` ##### Swap method Swap two items in the array: ```svelte ``` ##### Replace method Replace an item with new data: ```svelte ``` #### Nested field arrays If you need to nest multiple field arrays, the path array syntax makes it straightforward. Simply extend the path with additional array indices: ```ts const NestedSchema = v.object({ categories: v.array( v.object({ name: v.string(), items: v.array( v.object({ title: v.string(), }) ), }) ), }); ``` ```svelte {#snippet children(categoryArray)} {#each categoryArray.items as category, categoryIndex (category)}
{#snippet children(field)} {/snippet} {#snippet children(itemArray)} {#each itemArray.items as item, itemIndex (item)} {#snippet children(field)} {/snippet} {/each} {/snippet}
{/each} {/snippet}
``` You can nest field arrays as deeply as you like. You will also find a suitable example of this in our [playground](/playground/nested/). #### Field array validation As with fields, you can validate field arrays using Valibot's array validation functions. For example, to limit the length of the array: ```ts const TodoFormSchema = v.object({ todos: v.pipe( v.array( v.object({ label: v.string(), deadline: v.string(), }) ), v.minLength(1), v.maxLength(10) ), }); ``` The validation errors for the field array itself are available in `fieldArray.errors` and can be displayed alongside the array. ### TypeScript Since the library is written in TypeScript and we put a lot of emphasis on the development experience, you can expect maximum TypeScript support. Types are automatically inferred from your [Valibot schemas](/svelte/guides/define-your-form.md), providing type safety throughout your forms. #### Type inference Formisch uses Valibot's type inference to automatically derive TypeScript types from your schemas. You don't need to define separate types—they're inferred automatically. ```svelte
``` ##### Input and output types Valibot schemas can have different input and output types when using transformations. Formisch provides proper typing for both. ```svelte
{#snippet children(field)} {/snippet} {#snippet children(field)} {/snippet}
``` ##### Type-safe paths Field paths are fully type-checked. TypeScript will provide autocompletion and catch invalid paths at compile time. ```ts const UserSchema = v.object({ profile: v.object({ name: v.object({ first: v.string(), last: v.string(), }), email: v.string(), }), }); const userForm = createForm({ schema: UserSchema }); // ✓ Valid paths - TypeScript provides autocompletion // // // ✗ Invalid paths - TypeScript error // // ``` #### Type-safe props To pass your form to another component via props, you can use the [`FormStore`](/svelte/api/FormStore.md) type along with your schema type to get full type safety. ```svelte
``` #### Generic field components You can create generic field components with proper TypeScript typing using the [`FormStore`](/svelte/api/FormStore.md) type with Valibot's `GenericSchema`. ```svelte
{#if field.errors}
{field.errors[0]}
{/if}
``` The `v.GenericSchema<{ email: string }>` type ensures that the form passed to the component must have an `email` field of type `string`. TypeScript will catch any type mismatches at compile time. #### Available types Most types you need can be imported from `@formisch/svelte`. You can find all available types in our [API reference](/svelte/api/). - [`FormStore`](/svelte/api/FormStore.md) - The form store type - [`FieldStore`](/svelte/api/FieldStore.md) - The field store type - [`FieldArrayStore`](/svelte/api/FieldArrayStore.md) - The field array store type - [`ValidPath`](/core/api/ValidPath.md) - Type for valid field paths - [`ValidArrayPath`](/core/api/ValidArrayPath.md) - Type for valid array field paths - [`FormSchema`](/core/api/FormSchema.md) - Form schema type required at the root - [`Schema`](/core/api/Schema.md) - Base schema type from Valibot - [`SubmitHandler`](/core/api/SubmitHandler.md) - Type for submit handlers without the event - [`SubmitEventHandler`](/core/api/SubmitEventHandler.md) - Type for submit handlers with the event ### Architecture You don't need to read this guide to use Formisch. It's here for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Svelte, Angular, SolidJS, React, React Native, Vue, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices. #### Three packages, three responsibilities Formisch is structured as three packages, each with a clear role: - [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration. - [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: [`setInput`](/methods/api/setInput.md), [`validate`](/methods/api/validate.md), [`reset`](/methods/api/reset.md), [`insert`](/methods/api/insert.md), [`move`](/methods/api/move.md), [`focus`](/methods/api/focus.md) and so on. Each method lives in its own file and only imports the core helpers it actually needs. - [`@formisch/svelte`](https://github.com/open-circle/formisch/tree/main/frameworks/svelte) is the thin user-facing layer. It provides the [`createForm`](/svelte/api/createForm.md), [`useField`](/svelte/api/useField.md) and [`useFieldArray`](/svelte/api/useFieldArray.md) runes, the [`
`](/svelte/api/Form.md), [``](/svelte/api/Field.md) and [``](/svelte/api/FieldArray.md) components, and re-exports every method from `@formisch/methods` so you only import from a single package. ``` @formisch/svelte ├─ createForm, , , … (Svelte-specific reactive layer) └─ re-exports @formisch/methods/svelte (focus, reset, validate, …) └─ @formisch/core/svelte (createFormStore, types, signals) └─ framework adapter (createSignal, batch, untrack, createId) ``` The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability (`setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest) is exported as its own function. A form that only imports `createForm`, `` and `` ships nothing else, even though `@formisch/svelte` re-exports every method. #### One core, one adapter per framework The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/svelte`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`. The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.svelte.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined. For Svelte, [the adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.svelte.ts) is a thin wrapper around `$state.raw` that exposes the `Signal` shape Formisch's core relies on: ```ts export function createSignal(initialValue: T): Signal { let signal = $state.raw(initialValue); return { get value() { return signal; }, set value(nextValue: T) { signal = nextValue; }, }; } ``` For frameworks that don't have a native signal primitive (like React), the adapter implements a small pub/sub system from scratch that exposes the same shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional. This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction (a custom store, an observer protocol, a subscription layer) that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real `$state.raw` cells, integrated with Svelte's compiler-driven fine-grained tracking. That Formisch is framework-agnostic is not a trade-off you make. It's a benefit you get. A bug surfaced by a Solid user is a bug fixed for Svelte, React, Vue and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward. #### State lives in signals Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface: ```ts interface Signal { get value(): T; set value(nextValue: T): void; } ``` Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isEdited`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal (typically a single ``) re-render. This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object. They walk a typed tree of stores and update individual signals. #### What happens when you call `createForm` When you call [`createForm`](/svelte/api/createForm.md), three layers cooperate to produce the form store you receive. ```svelte ``` First, the [Svelte wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/svelte/src/runes/createForm/createForm.svelte.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts), together with a parse closure that captures your schema: ```ts const internalFormStore = createFormStore(config, (input) => v.safeParseAsync(config.schema, input) ); ``` Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy: - A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name. - A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes. - A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value. - Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into. - `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path. At every level, the same four signals are created: `errors`, `isTouched`, `isEdited` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`. Finally, the Svelte wrapper builds the public form store you actually receive. It's a small reactive object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isEdited`, `isDirty` and `isValid` is declared with `$derived` so it recomputes lazily as the underlying field signals change: ```ts const isTouched = $derived(getFieldBool(internalFormStore, 'isTouched')); const isDirty = $derived(getFieldBool(internalFormStore, 'isDirty')); const isValid = $derived(!getFieldBool(internalFormStore, 'errors')); return { [INTERNAL]: internalFormStore, get isSubmitting() { return internalFormStore.isSubmitting.value; }, get isTouched() { return isTouched; }, // … }; ``` The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) key. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while regular form code uses the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves. #### Why this design pays off Bundle size is the obvious payoff. The others are why the design is worth talking about: - **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/svelte`](https://github.com/open-circle/formisch/tree/main/frameworks/svelte), a form that uses only [`createForm`](/svelte/api/createForm.md) and [``](/svelte/api/Field.md) never pulls in [`validate`](/methods/api/validate.md), [`reset`](/methods/api/reset.md), [`insert`](/methods/api/insert.md) or the other operations. Tree-shaking gets to do real work. - **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and Svelte's compiler-driven tracking handles propagation. Updating one field does not invalidate the rest. - **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives. - **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync. #### Build on the core Use `@formisch/svelte/internals` to access the framework-specific core from your application or library. The [internals guide](/svelte/guides/internals.md) shows how to create custom methods and explains what to consider when building reactive bindings and components. #### Further reading To see how these design choices stack up against alternatives, head to the [comparison](/svelte/guides/comparison.md) guide. For a closer look at how Valibot's inference flows into your editor, see [TypeScript](/svelte/guides/typescript.md). And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch). ### Internals Formisch exposes its core through `@formisch/svelte/internals`. You can use these functions and types to build methods, reactive bindings and components for behavior that Formisch does not provide out of the box. Coding agents can use the same building blocks as the library itself. #### Versioning **Internals are not covered by Formisch's semantic versioning guarantees.** Breaking changes to their functions, types and store structure can occur in any release, including minor and patch releases. Review your use of internals when upgrading Formisch, and test validation, reactivity and cleanup for the behavior your extension implements. #### Import the core ```ts import { type BaseFormStore, getFieldStore, INTERNAL, setFieldInput, } from '@formisch/svelte/internals'; ``` This entry point re-exports the entire Svelte-specific core, including store types, field and array helpers, validation utilities and the reactivity adapter. It is included in `@formisch/svelte`, so no additional package is needed. Always use the internals from the same framework package as your form so they share its reactivity implementation. The regular `@formisch/svelte` entry point provides the ready-made methods and framework bindings. The internals entry point gives you access to the lower-level operations and store structure described in the [architecture guide](/svelte/guides/architecture.md). #### Implement a method The existing [`setInput`](/methods/api/setInput.md) method shows how these building blocks work together. This simplified implementation updates a single field and reuses `SetFieldInputConfig` to infer the path and input types from your schema: ```ts import type { SetFieldInputConfig } from '@formisch/svelte'; import { type BaseFormStore, batch, type FormSchema, getFieldStore, INTERNAL, type RequiredPath, setFieldInput, validateIfRequired, } from '@formisch/svelte/internals'; /** * Sets a field's input and validates it if required. */ export function setInput< TSchema extends FormSchema, TFieldPath extends RequiredPath, >( form: BaseFormStore, config: SetFieldInputConfig ): void { batch(() => { const internalFormStore = form[INTERNAL]; const internalFieldStore = getFieldStore(internalFormStore, config.path); if (internalFieldStore) { setFieldInput(internalFormStore, config.path, config.input); validateIfRequired(internalFormStore, internalFieldStore, 'input'); } }); } ``` Pass an existing form store, a field path and its new input to this method: ```ts setInput(loginForm, { path: ['email'], input: 'jane@example.com' }); ``` `INTERNAL` is the key for accessing the internal store through `form[INTERNAL]`. `getFieldStore` can return `undefined` when a dynamic array item no longer exists, so check the result before updating the field. `setFieldInput` updates the input and maintains touched, edited and dirty state. Writing directly to an input signal bypasses this bookkeeping. `batch` groups signal updates, and `validateIfRequired` triggers validation only when the form's validation settings require it for an input event. The [full implementation](https://github.com/open-circle/formisch/blob/main/packages/methods/src/setInput/setInput.ts) also supports setting the entire form input and includes the corresponding type overloads. Use the built-in method in your application; this example is a starting point for understanding the core helpers and building your own methods. #### Build reactive bindings and components You can also build your own framework bindings on top of the core. These bindings are responsible for subscribing to state changes, registering input elements or refs, handling events and cleaning up when fields unmount. Start with [input components](/svelte/guides/input-components.md) if you can compose the existing bindings. Read signals in a Svelte reactive context. Follow the existing runes for reactive paths, derived state, element registration and cleanup. The [Svelte binding source](https://github.com/open-circle/formisch/tree/main/frameworks/svelte/src) shows how Formisch handles these responsibilities. The core exposes the helpers those bindings use; it does not automatically supply their framework lifecycle behavior. #### Work with coding agents Give your agent this guide, your schema and the behavior you want to build. Ask it to inspect the relevant method or framework binding and the installed type declarations, then test the custom behavior with your form. The [coding agents guide](/svelte/guides/coding-agents.md) explains how to access the documentation through our skill, MCP server and Markdown files. ## Migration guides (guides) ### Migrate from Superforms This guide walks you through migrating a form from [Superforms](https://superforms.rocks) to Formisch. It covers the mental-model differences, shows the same form implemented in both libraries, and maps the Superforms API to its Formisch equivalents. Migrating is a rewrite of the form layer, not a drop-in replacement. Superforms is built around SvelteKit's server form actions, while Formisch is a pure client-side form layer. Migration typically means moving validation and submission out of `+page.server.ts` actions into client-side handlers, or wiring server validation separately. The good news: if you already validate with Valibot, your schemas carry over unchanged. Both libraries can coexist in the same application, so you can migrate one form at a time instead of rewriting everything at once. #### Key differences The biggest shift is where the form lives. Superforms is server-first: you initialize the form with `superValidate` in a `load` function, post to a form action, and connect the client with `superForm` and `use:enhance`. Even in SPA mode, the API keeps this server-shaped structure with a `SuperValidated` object and an `onUpdate` event. Formisch is client-first: you call the [`createForm`](/svelte/api/createForm.md) rune directly in your component, and there is no `load` function, form action, or adapter involved. Dedicated SvelteKit integration is planned; until then, server validation is something you wire up separately, for example by parsing the same Valibot schema in an API endpoint. Beyond that, three differences shape the migration: - **Schema handling**: Both libraries are schema-based, but Superforms supports many schema libraries through adapters like `valibot(schema)` and `valibotClient(schema)`. Formisch supports only Valibot and takes the schema directly, without an adapter. The schema is the single source of truth for types, validation, and form structure at once. - **Validation location and timing**: Superforms validates on the server by default, and client-side validation is opt-in via the `validators` option with timing controlled by `validationMethod`. Formisch always validates on the client against your schema, and timing is controlled by the `validate` and `revalidate` config. - **Reactivity**: Superforms exposes Svelte stores that hold the whole form, like `$form`, `$errors`, and `$tainted`, and you connect inputs with `bind:value={$form.email}`. Formisch is headless with fine-grained per-field reactivity: the [`Field`](/svelte/api/Field.md) component and [`useField`](/svelte/api/useField.md) rune expose a store for a single field, and you connect inputs by spreading `field.props`. Nested data works out of the box through path arrays, so there is no equivalent of the `dataType: 'json'` option. One trade-off to be aware of: because Formisch runs entirely on the client, your forms no longer submit without JavaScript. If progressive enhancement through form actions is a hard requirement, Superforms remains the better fit for that form. #### Side-by-side example The following login form has two validated fields and a submit handler. Both versions use the same Valibot schema. ##### Superforms Superforms splits the form across three files: the shared schema, the server route with the `load` function and form action, and the page component. ```ts // src/lib/schemas.ts import * as v from 'valibot'; export const LoginSchema = v.object({ email: v.pipe( v.string(), v.nonEmpty('Please enter your email.'), v.email('The email address is badly formatted.') ), password: v.pipe( v.string(), v.nonEmpty('Please enter your password.'), v.minLength(8, 'Your password must have 8 characters or more.') ), }); ``` ```ts // src/routes/login/+page.server.ts import { LoginSchema } from '$lib/schemas'; import { fail } from '@sveltejs/kit'; import { superValidate } from 'sveltekit-superforms'; import { valibot } from 'sveltekit-superforms/adapters'; export const load = async () => { const form = await superValidate(valibot(LoginSchema)); return { form }; }; export const actions = { default: async ({ request }) => { const form = await superValidate(request, valibot(LoginSchema)); if (!form.valid) { return fail(400, { form }); } // Log in the user with form.data return { form }; }, }; ``` ```svelte {#if $errors.email}
{$errors.email}
{/if} {#if $errors.password}
{$errors.password}
{/if} ``` ##### Formisch The same form in Formisch is a single component. The schema stays exactly the same. ```svelte
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
``` #### Migration steps ##### Install Formisch Install the Svelte package of Formisch. If you used the Valibot adapter with Superforms, Valibot is already installed; otherwise add it as well, since it is a peer dependency. ```bash npm install @formisch/svelte valibot ``` You can keep `sveltekit-superforms` installed while you migrate form by form and remove it at the end. ##### Move validation into a Valibot schema If your Superforms setup already uses Valibot, your schemas carry over unchanged. Just drop the adapter: instead of wrapping the schema in `valibot(...)` or `valibotClient(...)`, you pass it directly to [`createForm`](/svelte/api/createForm.md). If you used Zod or another schema library, rewrite the schema in Valibot. Most validations translate one to one. ```ts import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe(v.string(), v.nonEmpty(), v.email()), password: v.pipe(v.string(), v.nonEmpty(), v.minLength(8)), }); ``` Define the fields in the same order they appear in your form, since Formisch focuses the first invalid field on submit based on the schema order. See the [define your form](/svelte/guides/define-your-form.md) guide for details on how the schema shapes your form. ##### Replace the form setup Remove the `superValidate` call from your `load` function and the `data.form` plumbing, and replace `superForm` with the [`createForm`](/svelte/api/createForm.md) rune. Initial values that previously came from the `load` function move into the `initialInput` config. ```svelte ``` If your Superforms form used client-side `validators`, carry the timing over with `validate: 'blur'` and `revalidate: 'input'`, as explained in the [validation timing](#validation-timing) section below. Without these options, Formisch validates on submit first and then revalidates on every input. The form store returned by [`createForm`](/svelte/api/createForm.md) replaces the destructured stores of `superForm`: you access field state through the [`Field`](/svelte/api/Field.md) component and form state through properties like `loginForm.isSubmitting`. ##### Replace field bindings In Superforms, inputs bind to the central form store with `bind:value={$form.email}` and read errors from `$errors.email`. In Formisch, you wrap each input in a [`Field`](/svelte/api/Field.md) component, spread `field.props` onto the element, and set its `value` from `field.input`. The `name` attribute and all event handlers are included in `field.props`. ```svelte {#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
``` The `path` property is an array of keys, so nested fields like `$form.user.email` become `path={['user', 'email']}` without any `dataType: 'json'` configuration. For fields with a non-string schema like `v.number()` or `v.boolean()`, see the [controlled fields](/svelte/guides/controlled-fields.md) and [special inputs](/svelte/guides/special-inputs.md) guides. ##### Update submission handling Replace the form action and `use:enhance` with the [`Form`](/svelte/api/Form.md) component and its `onsubmit` property. Where Superforms posts to a form action and processes the result in `onUpdate` or `onUpdated`, Formisch validates on the client and calls your handler with the typed output of your schema. Logic from your form action moves into this handler, for example as a `fetch` call to an API endpoint. ```svelte
``` If you still need server-side validation, parse the submitted data in your endpoint with the same Valibot schema. This keeps a single source of truth for both sides. See the [handle submission](/svelte/guides/handle-submission.md) guide for async submission and error handling patterns. #### API mapping | Superforms | Formisch | | ------------------------------------------ | --------------------------------------------------------------------------------------- | | `superValidate(valibot(schema))` in `load` | Not needed, [`createForm`](/svelte/api/createForm.md) runs on the client | | `superForm(data.form, options)` | [`createForm`](/svelte/api/createForm.md) rune | | `defaults(data, valibotClient(schema))` | `initialInput` config of [`createForm`](/svelte/api/createForm.md) | | `validators: valibotClient(schema)` | `schema` config, validation always comes from the schema | | `validationMethod` | `validate` / `revalidate` config | | `dataType: 'json'` | Not needed, path arrays support nested data | | `bind:value={$form.email}` | [`Field`](/svelte/api/Field.md) with `field.props` and `field.input` | | `$errors.email` | `field.errors` | | `$allErrors` | [`getDeepErrorEntries`](/methods/api/getDeepErrorEntries.md) | | `$constraints` | No equivalent (see notes) | | `use:enhance` with form action | [`Form`](/svelte/api/Form.md) with `onsubmit` | | `onUpdate` / `onUpdated` events | [`SubmitHandler`](/core/api/SubmitHandler.md) passed to `onsubmit` | | `$submitting` | `form.isSubmitting` | | `$delayed` / `$timeout` | No equivalent (see notes) | | `$posted` | `form.isSubmitted` | | `$tainted` / `isTainted('email')` | `field.isDirty` / [`isDirty`](/methods/api/isDirty.md) | | `reset({ data, newState })` | [`reset`](/methods/api/reset.md) with `initialInput` | | `validateForm()` / `validate('email')` | [`validate`](/methods/api/validate.md) | | `submit()` | [`submit`](/methods/api/submit.md) | | `setError(form, 'email', message)` | [`setErrors`](/methods/api/setErrors.md) | | `$message` / `setMessage` | No equivalent (see notes) | | `taintedMessage` | No equivalent (see notes) | A few entries have no direct counterpart: - **`$constraints`**: Superforms derives HTML validation attributes like `required` and `minlength` from the schema. Formisch validates at runtime against the schema instead and does not generate constraint attributes. Set attributes like `required` on your inputs yourself where you want them for accessibility or styling. - **`$delayed` / `$timeout`**: Formisch exposes `isSubmitting` and `isValidating` but has no built-in loading timers. If you need a delayed spinner, wrap `form.isSubmitting` in your own timer logic. - **`$message` / `setMessage`**: Status messages are not form state in Formisch. Use regular Svelte state that you set in your submit handler. - **`taintedMessage`**: There is no built-in navigation guard. You can build one with SvelteKit's `beforeNavigate` and the [`isDirty`](/methods/api/isDirty.md) method. Unlike Superforms, Formisch does not automatically update the comparison baseline after a successful server response; call [`reset`](/methods/api/reset.md) when the saved values should become the new baseline. - **`validate('email')`**: Formisch has no partial validation. The [`validate`](/methods/api/validate.md) method always parses the whole form against the schema, which keeps cross-field rules correct, and then distributes the errors to the individual fields. #### Common patterns ##### Form state Superforms spreads form state across several stores like `$submitting`, `$posted`, and `$tainted` with the `isTainted` helper. In Formisch, the form store exposes reactive properties directly: `isSubmitting`, `isSubmitted`, `isValidating`, `isTouched`, `isEdited`, `isDirty`, `isValid`, and `errors`. Each field offers per-field `isTouched`, `isEdited`, `isDirty`, and `isValid` counterparts. Superforms' tainted state tracks the original data and clears when a value is reverted, so the comparison-based `isDirty` is the closest match; use `isEdited` when you want sticky tracking that stays on after the first change. ```svelte ``` To check the dirty state of a single field outside of a [`Field`](/svelte/api/Field.md) component, like `isTainted('email')` in Superforms, use the [`isDirty`](/methods/api/isDirty.md) method. Wrap the call in `$derived` so it stays reactive: ```ts import { isDirty } from '@formisch/svelte'; const emailIsDirty = $derived(isDirty(loginForm, { path: ['email'] })); ``` ##### Validation timing The `validationMethod` option of Superforms maps onto the `validate` and `revalidate` config of [`createForm`](/svelte/api/createForm.md). The closest match for the default `'auto'` behavior is `validate: 'blur'` with `revalidate: 'input'`: ```ts const loginForm = createForm({ schema: LoginSchema, validate: 'blur', // 'onblur' part of Superforms 'auto' revalidate: 'input', // 'oninput' part of Superforms 'auto' }); ``` Superforms remembers that a field previously had errors and continues validating it on input even after the errors clear. Formisch's revalidation trigger depends on the mode: with the default `validate: 'submit'`, the form revalidates on input once it has been submitted. With modes like `'blur'` or `'input'`, a field with errors revalidates on input and returns to the initial trigger once its errors clear. The explicit modes map directly: `'oninput'` to `validate: 'input'`, `'onblur'` to `validate: 'blur'`, and `'onsubmit'` to `validate: 'submit'`, which is the Formisch default. Note that `validate` and `revalidate` control when validation runs, not when errors are shown. The [validation](/svelte/guides/validation.md) guide shows how to reveal errors at the right time using flags like `field.isEdited` and `form.isSubmitted`. ##### Field arrays In Superforms, nested arrays of objects generally use `dataType: 'json'`, an `{#each}` loop over `$form.tags`, and manual array mutations like `$form.tags = [...$form.tags, { name: '' }]`; top-level arrays of primitive values can instead be posted as repeated form fields. Formisch provides the [`FieldArray`](/svelte/api/FieldArray.md) component, which exposes stable `items` keys for the `{#each}` block, together with methods like [`insert`](/methods/api/insert.md), [`remove`](/methods/api/remove.md), [`move`](/methods/api/move.md), and [`swap`](/methods/api/swap.md): ```svelte {#snippet children(fieldArray)} {#each fieldArray.items as item, index (item)} {#snippet children(field)} {/snippet} {/each} {/snippet} ``` See the [field arrays](/svelte/guides/field-arrays.md) guide for the full API. ##### Reset The `reset()` method of Superforms maps to the [`reset`](/methods/api/reset.md) method of Formisch. Calling it without a config restores the initial values. Passing `initialInput` changes both the current values and the baseline used by future resets. Superforms separates those concerns: `data` changes the values for that reset, while `newState` changes the future reset baseline, so use both there when you want the same result as Formisch's `initialInput`: ```ts import { reset } from '@formisch/svelte'; // Reset to the initial values reset(loginForm); // Reset to new values that become the new baseline reset(loginForm, { initialInput: { email: 'new@example.com' } }); ``` There is no `resetForm` option because Formisch does not reset the form after submission by default. Superforms v2 enables `resetForm` by default, so if your form relied on that behavior, call [`reset`](/methods/api/reset.md) after a successful Formisch submission. ##### Special inputs Superforms binds special input types directly to the `$form` store, for example with `bind:checked={$form.cookies}` for a checkbox. In Formisch, you spread `field.props` and set the state attribute that matches the input type: ```svelte {#snippet children(field)} {/snippet} ``` Checkboxes, radio buttons, selects and file inputs are covered in the [special inputs](/svelte/guides/special-inputs.md) guide. One thing to watch out for: Formisch's event handlers read values from the DOM as strings, so fields with a non-string schema like `v.number()` or `v.date()` must be controlled as described in the [controlled fields](/svelte/guides/controlled-fields.md) guide. #### Next steps You now have everything you need to migrate your forms. To deepen your understanding of Formisch, work through the main concepts starting with [define your form](/svelte/guides/define-your-form.md) and [add form fields](/svelte/guides/add-form-fields.md). The [validation](/svelte/guides/validation.md) guide covers error display in depth, the [form methods](/svelte/guides/form-methods.md) guide lists all available methods, and the [controlled fields](/svelte/guides/controlled-fields.md) guide explains how to handle non-string values. ### Migrate from TanStack Form This guide walks you through migrating a form from [TanStack Form](https://tanstack.com/form/latest) (`@tanstack/svelte-form`) to Formisch. It covers the mental-model differences, a complete side-by-side example, a step-by-step migration path, and a mapping of TanStack Form APIs to their Formisch equivalents. Migrating is a rewrite of the form layer, not a drop-in replacement. The two libraries structure forms differently: TanStack Form configures validation per field and reads state through subscriptions, while Formisch derives everything from a single Valibot schema. The form markup and your submission logic usually carry over with small changes. Both libraries can coexist in the same application, so you can migrate one form at a time and remove `@tanstack/svelte-form` once the last form is converted. #### Key differences TanStack Form infers its types from the `defaultValues` object you pass to `createForm`. Validation is attached separately: each field (or the form itself) receives a `validators` object keyed by trigger, such as `onChange`, `onBlur`, `onSubmit` or `onMount`, with async variants like `onChangeAsync`. A validator can be a function or a Standard Schema, so a Valibot schema can already appear in a TanStack Form codebase, but it is one validator among many rather than the definition of the form. Formisch is schema-first. A single Valibot schema is the source of the form's TypeScript types, its runtime validation, and its structure, all at once. There is no `defaultValues` object to keep aligned with a separate type, and no per-field validator wiring. Validation always parses the entire form against the schema, so cross-field rules work without extra setup, and its timing is controlled form-wide by the `validate` and `revalidate` config. The remaining differences you will notice during migration: - **Field addressing**: TanStack Form uses string names like `"todos[0].label"`. Formisch uses type-safe path arrays like `['todos', 0, 'label']` that TypeScript checks against your schema. - **Input binding**: TanStack Form wires `value`, `oninput` and `onblur` by hand through `field.handleChange` and `field.handleBlur`. Formisch bundles the event handlers into `field.props`, which you spread onto the input element. - **Reactivity**: TanStack Form reads state through explicit subscriptions (`form.Subscribe`, `form.useStore`, now `form.useSelector`) with selector functions. Formisch state is fine-grained through Svelte 5 runes, so you read properties like `form.isSubmitting` directly and only the affected parts of the UI update. - **Submission**: TanStack Form calls the `onSubmit` option with the current values. Formisch validates against the schema first and calls your `onsubmit` handler only with typed, validated output. #### Side-by-side example The same login form with two validated fields and a submit handler, first in TanStack Form, then in Formisch. ##### TanStack Form ```svelte
{ event.preventDefault(); event.stopPropagation(); form.handleSubmit(); }} > !value ? 'Please enter your email.' : !/^\S+@\S+\.\S+$/.test(value) ? 'The email address is badly formatted.' : undefined, }} > {#snippet children(field)} field.handleChange(event.currentTarget.value)} /> {#if field.state.meta.errors.length}
{field.state.meta.errors[0]}
{/if} {/snippet}
!value ? 'Please enter your password.' : value.length < 8 ? 'Your password must have 8 characters or more.' : undefined, }} > {#snippet children(field)} field.handleChange(event.currentTarget.value)} /> {#if field.state.meta.errors.length}
{field.state.meta.errors[0]}
{/if} {/snippet}
state.isSubmitting}> {#snippet children(isSubmitting)} {/snippet}
``` ##### Formisch ```svelte
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
{#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
``` The validation rules, error messages and types now live in one schema, the input wiring collapses into `{...field.props}`, and form state is read directly from the store without a subscription component. #### Migration steps ##### Install Formisch Add Formisch and Valibot to your project. Valibot is a peer dependency: ```bash npm install @formisch/svelte valibot ``` Keep `@tanstack/svelte-form` installed until all forms are migrated. See the [installation](/svelte/guides/installation.md) guide for other package managers. ##### Move validation into a Valibot schema Collect the rules from your `validators` objects and express them as one Valibot schema. Each per-field validator becomes a validation action in the schema, together with its error message. The `onBlur` validator of the email field above becomes: ```ts import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe( v.string(), v.nonEmpty('Please enter your email.'), v.email('The email address is badly formatted.') ), // ... }); ``` If your TanStack Form validators already use Valibot through Standard Schema, you can often merge the per-field schemas into a single object schema and reuse them as they are. Define the fields in the same order they appear in your form, since Formisch focuses the first invalid field on submit based on the schema order. See the [define your form](/svelte/guides/define-your-form.md) guide for details. ##### Replace the form setup TanStack Form's `createForm` takes a function returning options with `defaultValues` and `onSubmit`. The Formisch [`createForm`](/svelte/api/createForm.md) rune takes a plain config object with your schema: ```ts const loginForm = createForm({ schema: LoginSchema, validate: 'blur', }); ``` You no longer need a `defaultValues` object just to establish types. Required string fields start as an empty string automatically. If you prefilled fields with `defaultValues`, pass those values as `initialInput`. The submission handler moves to the [`Form`](/svelte/api/Form.md) component in the next steps. ##### Replace field bindings Replace `form.Field` with the [`Field`](/svelte/api/Field.md) component. The string `name` becomes a type-safe `path` array, the manual event wiring becomes a spread of `field.props`, and errors move from `field.state.meta.errors` to `field.errors`: ```svelte {#snippet children(field)} {#if field.errors}
{field.errors[0]}
{/if} {/snippet}
``` For field components built with TanStack Form's form composition (`useFieldContext`), the Formisch counterpart is the [`useField`](/svelte/api/useField.md) rune, which gives you the same field store inside your own components. See the [add form fields](/svelte/guides/add-form-fields.md) and [input components](/svelte/guides/input-components.md) guides. ##### Update submission handling Replace the native `
` element and the manual `form.handleSubmit()` call with the [`Form`](/svelte/api/Form.md) component. It renders a `` element, calls `event.preventDefault()` for you, and only invokes your handler after the schema validation passes: ```svelte
``` Unlike TanStack Form's `onSubmit`, which receives the current form values, the Formisch handler receives the parsed schema output. See the [handle submission](/svelte/guides/handle-submission.md) guide to learn more. #### API mapping | TanStack Form | Formisch | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `createForm(() => ({ … }))` | [`createForm({ schema })`](/svelte/api/createForm.md) | | `defaultValues` | `initialInput` config | | `onSubmit` option | `onsubmit` prop of [`Form`](/svelte/api/Form.md) | | `validators` config | Valibot schema plus `validate` / `revalidate` config | | `` | [`Field`](/svelte/api/Field.md) with `path` array | | `field.state.value` | `field.input` | | `field.handleChange` / `handleBlur` | Spread `field.props`, or `field.onInput` for custom inputs | | `field.state.meta.errors` | `field.errors` | | `field.state.meta.isTouched` | `field.isTouched` | | `field.state.meta.isDirty` | `field.isEdited` | | `field.state.meta.isDefaultValue` | `!field.isDirty` | | `form.Subscribe` / `form.useStore` (now `form.useSelector`) | Direct property access, e.g. `form.isSubmitting` | | `state.isSubmitting` | `form.isSubmitting` | | `state.canSubmit` | No direct equivalent (see below) | | `form.handleSubmit()` | [`submit(form)`](/methods/api/submit.md) | | `form.reset()` | [`reset(form)`](/methods/api/reset.md) | | `form.setFieldValue(name, value)` | [`setInput(form, { path, input })`](/methods/api/setInput.md) | | `form.state.values` | [`getInput(form)`](/methods/api/getInput.md) | | `form.validateAllFields` | [`validate(form)`](/methods/api/validate.md) | | `` | [`FieldArray`](/svelte/api/FieldArray.md) | | `field.pushValue` / `insertValue` | [`insert(form, { path })`](/methods/api/insert.md) | | `field.removeValue` | [`remove(form, { path, at })`](/methods/api/remove.md) | | `field.moveValue` | [`move(form, { path, from, to })`](/methods/api/move.md) | | `field.swapValues` | [`swap(form, { path, at, and })`](/methods/api/swap.md) | | `field.replaceValue` | [`replace(form, { path, at, initialInput })`](/methods/api/replace.md) | A few entries deserve a short note: - **`canSubmit`**: Formisch does not compute a "can submit" flag. The common pattern is to keep the submit button enabled: on submit, Formisch validates the whole form and automatically focuses the first field with an error. If you still want to reflect validity in the UI, `form.isValid` reflects the result of the last validation run, but with the default `validate: 'submit'` timing it stays `true` until the first submit, so combine it with `validate: 'initial'` for TanStack-like behavior. - **Dirty state**: TanStack Form's `isDirty` is persistent (it stays `true` after a change is reverted), which matches Formisch's `isEdited`. Formisch's `isDirty` compares the current input against the initial input, which matches TanStack Form's `!isDefaultValue`. - **`onMount` validator**: use `validate: 'initial'` in the Formisch config to validate as soon as the form is created. - **Async validators**: there is no counterpart to `onChangeAsyncDebounceMs`. Async checks live in the schema via `v.pipeAsync` and `v.checkAsync`, and the form-level `form.isValidating` reflects the in-flight state (Formisch does not track validating state per field, because validation always parses the whole schema). #### Common patterns ##### Form state TanStack Form reads reactive state through the `form.Subscribe` component or the `form.useStore` hook (now `form.useSelector`) with a selector, as shown in the side-by-side example above. In Formisch, the form store returned by the [`createForm`](/svelte/api/createForm.md) rune is reactive through Svelte 5 runes, so you access properties directly and only the affected parts of the DOM update: ```svelte ``` The form store exposes `isSubmitting`, `isSubmitted`, `isValidating`, `isTouched`, `isEdited`, `isDirty`, `isValid` and `errors`. ##### Validation timing In TanStack Form, timing is part of each validator: the same rule behaves differently depending on whether you register it as `onChange`, `onBlur` or `onSubmit`. In Formisch, the rules live in the schema and the timing is configured once for the whole form: ```ts const loginForm = createForm({ schema: LoginSchema, validate: 'blur', // first validation on blur revalidate: 'input', // revalidate on every keystroke afterwards }); ``` `validate` controls when a field is first validated (`'initial'`, `'touch'`, `'input'`, `'change'`, `'blur'` or `'submit'`, defaulting to `'submit'`), and `revalidate` controls subsequent runs once a field has an error or the form was submitted (defaulting to `'input'`). To reveal errors only at the right moment, combine `field.isEdited` with `form.isSubmitted` as shown in the [validation](/svelte/guides/validation.md) guide. ##### Field arrays TanStack Form handles arrays with `mode="array"` and helper methods on the field: ```svelte {#snippet children(todosField)} {#each todosField.state.value as _, index} {#snippet children(field)} field.handleChange(event.currentTarget.value)} /> {/snippet} {/each} {/snippet} ``` Formisch provides the [`FieldArray`](/svelte/api/FieldArray.md) component, whose `items` array contains stable IDs for keying the `{#each}` block, together with standalone methods like [`insert`](/methods/api/insert.md) and [`remove`](/methods/api/remove.md): ```svelte {#snippet children(fieldArray)} {#each fieldArray.items as item, index (item)} {#snippet children(field)} {/snippet} {/each} {/snippet} ``` Constraints such as a minimum or maximum number of items move into the schema, for example with `v.nonEmpty()` and `v.maxLength(10)` on the array. See the [field arrays](/svelte/guides/field-arrays.md) guide for the full picture. ##### Reset TanStack Form resets with `form.reset()`, or `form.reset(values)` to also update the default values. Formisch uses the [`reset`](/methods/api/reset.md) method: ```ts import { reset } from '@formisch/svelte'; // Reset the form to its initial input reset(loginForm); // Reset and replace the initial input (the new dirty baseline) reset(loginForm, { initialInput: { email: 'user@example.com' } }); ``` Passing `initialInput` also updates the baseline for dirty tracking, just like passing values to TanStack Form's `reset` updates its default values. ##### Special inputs In TanStack Form, every input type is wired manually by passing the parsed value to `field.handleChange`, for example toggling a boolean for a checkbox. In Formisch, you spread `field.props` and set the state attribute that matches the input type: ```svelte {#snippet children(field)} {/snippet} ``` Checkboxes, radio buttons, selects and file inputs are covered in the [special inputs](/svelte/guides/special-inputs.md) guide. One thing to watch out for: Formisch's event handlers read values from the DOM as strings, so fields with a non-string schema like `v.number()` or `v.date()` must be controlled as described in the [controlled fields](/svelte/guides/controlled-fields.md) guide. Custom components that don't expose a native element use `field.onInput` instead of `field.props`. #### Next steps With your first form migrated, work through the main concepts to solidify the Formisch mental model: [define your form](/svelte/guides/define-your-form.md), [create your form](/svelte/guides/create-your-form.md), [add form fields](/svelte/guides/add-form-fields.md) and [handle submission](/svelte/guides/handle-submission.md). The [form methods](/svelte/guides/form-methods.md) guide lists everything that replaces TanStack Form's imperative API, and the [validation](/svelte/guides/validation.md) guide covers error timing and async checks in depth. ## Integration guides (guides) ### shadcn-svelte [shadcn-svelte](https://shadcn-svelte.com) generates accessible components into your own project, where you can adapt them to your needs. This guide targets shadcn-svelte v1 with its Bits UI v2 based components. No adapter package is needed: components that render a native element take the whole `field.props` spread, while the button based primitives receive the value plus the lifecycle props they forward. #### Installation Install Formisch and Valibot, set up Tailwind CSS, then initialize shadcn-svelte and add the components used below: ```bash npm install @formisch/svelte valibot npx shadcn-svelte@latest init npx shadcn-svelte@latest add button input label textarea checkbox select radio-group slider ``` The `init` command asks for a design system preset interactively, so run it in a terminal rather than a script. It also expects the `$lib` alias to exist. In a Vite project, add `resolve.alias` for `$lib` in `vite.config.ts` and a matching `paths` entry in `tsconfig.json`. #### Wiring patterns Choose the wiring from the element the generated component renders: - If it forwards its props to a native `` or `