# Formisch
> The lightweight, schema-first and fully type-safe form library for Angular, Preact, Qwik, React, React Native, Solid, Svelte and Vue.
## Get started
### Introduction
Formisch is a schema-based, headless form library for Vue. 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 [`useForm`](/vue/api/useForm.md) composable. It initializes your form's store based on the provided Valibot schema and infers its types. Next, wrap your form in the [`
`](/vue/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`](/vue/api/useField.md) composable or the [``](/vue/api/Field.md) component to connect your inputs.
```vue
```
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 VeeValidate, FormKit, and TanStack Form, see the [comparison guide](/vue/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/vue # npm
yarn add @formisch/vue # yarn
pnpm add @formisch/vue # pnpm
bun add @formisch/vue # bun
deno add npm:@formisch/vue # deno
```
Then you can import it into any JavaScript or TypeScript file.
```ts
import { … } from '@formisch/vue';
```
#### 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 Vue 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-vue.txt`](/llms-vue.txt) contains a table of contents with links to Vue-related files
- [`llms-vue-full.txt`](/llms-vue-full.txt) contains the Markdown content of the entire Vue docs
- [`llms-vue-guides.txt`](/llms-vue-guides.txt) contains the Markdown content of the Vue guides
- [`llms-vue-api.txt`](/llms-vue-api.txt) contains the Markdown content of the Vue 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. `/vue/guides/installation/` becomes [`/vue/guides/installation.md`](/vue/guides/installation.md)) or request the page with an `Accept: text/markdown` header to receive its Markdown version.
### Comparison
Formisch is one of several form libraries available for Vue. The three most common alternatives are [VeeValidate](https://vee-validate.logaretm.com), [FormKit](https://formkit.com), 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** | VeeValidate | FormKit | TanStack Form |
| ---------------------- | -------------------------------------------------------------- | ----------------------------- | ------------------------------------- | --------------------------------------- |
| Type source | Inferred from schema | Inferred from schema | Declared manually | Inferred from `defaultValues` |
| Validation location | Defined in schema | Validator rules or schema | Per-input prop or form schema | Per-validator config |
| Validation timing | Form-wide `validate` / `revalidate` | Per field, configurable | Per input (`validation-visibility`) | Per-validator trigger |
| Async validation | Built-in via schema | Built-in | Built-in | Built-in `isValidating` |
| Reactivity scope | Per Vue ref subscription | Per Vue ref subscription | Per Vue ref subscription | Per TanStack Store subscription |
| Schema libraries | Valibot | Zod, Yup, Valibot, ArkType, … | Built-in rules; schemas via plugins | Standard Schema |
| UI approach | Headless | Headless | Component-driven (batteries included) | Headless |
| Bundle size (min+gzip) | From ~2.5 kB | ~12 kB | ~25 kB+ | ~15 kB |
| Framework support | Angular, React, React Native, Preact, Solid, Svelte, Vue, Qwik | Vue 3 (Vue 2 via legacy) | Vue 3 | 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?
**VeeValidate** is the most widely used Vue form library. It is headless, composables-based, and has strong schema support through Standard Schema. Good fit when you want a flexible logic layer and are comfortable wiring your own components.
**FormKit** is component-driven and batteries-included. Inputs, layouts, accessibility, and styling come out of the box, with optional declarative form schemas. Best when you want a complete form solution with UI rather than a headless logic layer.
**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 Vue 3's composition API. The main consideration is that Formisch currently supports only [Valibot](https://valibot.dev) as the schema library.
#### Migrating to Formisch
Migrating to Formisch is not a drop-in replacement, but the libraries can coexist in the same application, so you can migrate one form at a time. The main work is consolidating validation rules into a single root Valibot schema and replacing the previous library's form and field APIs with Formisch's composables.
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 VeeValidate](/vue/guides/migrate-from-vee-validate.md), [migrate from FormKit](/vue/guides/migrate-from-formkit.md), and [migrate from TanStack Form](/vue/guides/migrate-from-tanstack-form.md).
#### Next steps
If you have decided that Formisch is a good fit, install it via the [installation](/vue/guides/installation.md) guide and start building by [defining your form](/vue/guides/define-your-form.md).
## Main concepts
### 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 [`useForm`](/vue/api/useForm.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](/vue/guides/create-your-form.md) guide to learn how to initialize your form with [`useForm`](/vue/api/useForm.md).
### Create your form
Formisch consists of composables, components and methods. To create a form you use the [`useForm`](/vue/api/useForm.md) composable.
#### Form composable
The [`useForm`](/vue/api/useForm.md) composable initializes and returns the store of your form. The store contains the state of the form and can be used with other Formisch composables, components and methods to build your form.
```vue
```
##### Configuration options
The [`useForm`](/vue/api/useForm.md) composable 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 = useForm({
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 = useForm({
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:
```vue
```
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](/vue/guides/add-form-fields.md) guide to learn how to connect your input elements to the form using the [`Field`](/vue/api/Field.md) component.
### Add form fields
To add a field to your form, you can use the [`Field`](/vue/api/Field.md) component or the [`useField`](/vue/api/useField.md) composable. Both are headless and provide access to field state for building your form UI.
#### Field component
The [`Field`](/vue/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.
##### v-slot directive
As a child, you use the `v-slot` directive to access the field store, which includes the current value, error messages, and props to spread onto your input element.
```vue
```
> **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 `v-model`, `checked`, or `selected`). See the [controlled fields](/vue/guides/controlled-fields.md) guide to learn more.
##### Headless design
The [`Field`](/vue/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:
```vue
```
For nested fields, the path reflects the structure of your schema:
```vue
```
##### Type safety
The API design of the [`Field`](/vue/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 composable
For very complex forms where you create individual components for each form field, Formisch provides the [`useField`](/vue/api/useField.md) composable. It allows you to access the field state directly within your component logic.
```vue
{{ field.errors[0] }}
```
##### When to use which
- **Use `Field` component**: When defining multiple fields in the same component. It ensures you don't accidentally access the wrong field store.
- **Use `useField` composable**: When creating field components for single fields. It allows you to access field state in your component logic.
The [`Field`](/vue/api/Field.md) component is essentially a thin wrapper around [`useField`](/vue/api/useField.md) that allows you to access the field state within template 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, ref callback, 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.
#### Next steps
Now that you know how to add fields to your form, continue to the [input components](/vue/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/vue/src/components).
#### Why input components?
Currently, your fields might look something like this:
```vue
{{ field.errors[0] }}
```
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:
```vue
```
#### 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. The `props` object from your field contains all the necessary event handlers and attributes.
```vue
```
##### Template code
After that, you can add the template code.
```vue
{{ errors[0] }}
```
##### 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.
```vue
{{ errors[0] }}
```
#### Using component libraries
Component libraries render their own components instead of plain elements, so you cannot use `v-bind="field.props"` on them. Vue treats the `ref` entry inside that object as a real template ref, which would register the component instance rather than a DOM element. Bind the entries individually and set the value through `v-model`:
```vue
```
Assigning `field.input` updates the field value and triggers validation, while the focus handler is what marks the field as touched. Unwrapping `$el` only helps when the component's root is the focusable element; if it wraps its control in a container, reach for the library's own input ref instead, or accept that [`focus`](/methods/api/focus.md) cannot target that field. For more details, see the [controlled fields](/vue/guides/controlled-fields.md) guide.
For ready-made wiring recipes, check out our integration guides:
- [shadcn-vue](/vue/guides/shadcn-vue.md)
- [Ark UI](/vue/guides/ark-ui.md)
- [Reka UI](/vue/guides/reka-ui.md)
- [PrimeVue](/vue/guides/primevue.md)
#### Next steps
Now that you know how to create reusable input components, continue to the [handle submission](/vue/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 `@submit` event of the [`Form`](/vue/api/Form.md) component. The first parameter passed to the function contains the validated form values.
```vue
```
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:
```vue
```
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:
```ts
const submitForm: SubmitHandler = async (values) => {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
if (response.ok) {
// Handle successful login
console.log('Login successful!');
} else {
// Handle error
console.error('Login failed');
}
} catch (error) {
console.error('Error during submission:', error);
}
};
```
##### 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:
```vue
```
#### Submit without \