From bd9240a88dbd20649319594faf790e9e548dfff2 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 01:39:56 +0000 Subject: [PATCH 1/5] Blog: Add dynamic invoice form guide with Svelte 5 and Formisch --- .../dynamic-invoice-form-svelte/index.mdx | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx new file mode 100644 index 00000000..61104f9a --- /dev/null +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -0,0 +1,510 @@ +--- +title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' +description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. +authors: + - flySewa +--- + + + +# Building a Dynamic Invoice Form in Svelte 5 with Formisch + +Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. + +A field is not only a value. It has validation state, errors, transformations, and a relationship with the rest of the form. When a form grows beyond a handful of inputs, things like nested data, dynamic fields, and derived values all need to stay in sync. + +At this point, form state management moves away from handling individual inputs and towards managing the entire form model. + +Formisch approaches this by treating the form as a connected structure. It's a headless form library designed to stay lightweight while keeping the schema, state, validation, and submitted output connected. The schema, state, validation, and submitted output all come from the same source, while still letting you control the UI. + +To demonstrate this, we'll build a dynamic invoice form and look at how Formisch works with Svelte 5's rune-based reactivity to manage: + +- Invoice details +- Client information +- Dynamic line items +- Validation +- Live totals +- Typed submission output + +By the end, you'll have a working invoice form that handles nested data, dynamic fields, validation, and typed output. + +The focus here is on the form architecture: how state, validation, and dynamic fields fit together. The styling and project setup are kept minimal on purpose. + +**Prerequisites:** This tutorial assumes you already have a Svelte 5 project set up and are familiar with Svelte components and runes. + +We'll be using: + +- Svelte 5 +- [Formisch](https://formisch.dev) +- [Valibot](https://valibot.dev) for schema validation + +You can follow along here, or view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte) + +--- + +## Step 1: Install dependencies + +We'll start by installing Formisch and Valibot: + +```bash +npm install @formisch/svelte valibot +``` + +--- + +## Step 2: Define the form model + +Before connecting inputs, we need to define what our invoice form looks like. + +The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. + +Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. + +Our invoice needs basic metadata, client details, and a list of line items. We'll define those requirements with Valibot: + +```javascript +import * as v from 'valibot'; + +const moneyInput = (message) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(0, 'Amount cannot be negative') + ); + +const positiveNumberInput = (message) => + v.pipe( + v.string(), + v.nonEmpty(message), + v.toNumber(), + v.minValue(1, 'Value must be at least 1') + ); + +const InvoiceSchema = v.object({ + invoiceNumber: v.pipe(v.string(), v.nonEmpty('Invoice number is required')), + + issueDate: v.pipe(v.string(), v.nonEmpty('Issue date is required')), + + dueDate: v.pipe(v.string(), v.nonEmpty('Due date is required')), + + client: v.object({ + name: v.pipe(v.string(), v.nonEmpty('Client name is required')), + + email: v.pipe( + v.string(), + v.nonEmpty('Client email is required'), + v.email('Enter a valid email address') + ) + }), + + lineItems: v.pipe( + v.array( + v.object({ + description: v.pipe( + v.string(), + v.nonEmpty('Description is required') + ), + + quantity: positiveNumberInput('Quantity is required'), + + unitPrice: moneyInput('Unit price is required') + }) + ), + + v.minLength(1, 'Add at least one invoice item'), + v.maxLength(10, 'You can only add up to 10 invoice items') + ), + + taxRate: v.pipe( + v.string(), + v.nonEmpty('Tax rate is required'), + v.toNumber(), + v.minValue(0, 'Tax cannot be negative'), + v.maxValue(100, 'Tax cannot be more than 100%') + ), + + discount: moneyInput('Discount is required'), + + notes: v.optional(v.string()) +}); +``` + +The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. + +Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. + +The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. + +This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. + +--- + +## Step 3: Create the form + +With the form model defined, we can create the form state from that schema. + +`createForm` connects the schema to the reactive form state. From this point, Formisch can use the same structure for fields, validation, and submission. + +```javascript +import { createForm } from '@formisch/svelte'; + +const invoiceForm = createForm({ + schema: InvoiceSchema, + + initialInput: { + invoiceNumber: 'INV-001', + + issueDate: new Date() + .toISOString() + .slice(0, 10), + + dueDate: '', + + client: { + name: '', + email: '' + }, + + lineItems: [ + { + description: '', + quantity: '1', + unitPrice: '0' + } + ], + + taxRate: '7.5', + discount: '0', + notes: '' + } +}); +``` + +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. + +Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. + +With the form created, the next step is to connect individual fields to that state. + +--- + +## Step 4: Connect fields + +Formisch is headless by design. It does not decide what your inputs should look like. Instead, it gives you the state and bindings needed to connect your own markup. + +A field is connected through the `Field` component: + +```svelte + + {#snippet children(field)} + + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+``` + +The `path` tells Formisch where this field exists in the form model. For nested values, the path follows the same structure as the schema: + +```svelte + +``` + +This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. + +You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. + +--- + +## Step 5: Add dynamic line items + +Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. + +Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. + +```svelte + + {#snippet children(fieldArray)} + {#each fieldArray.items as item, index (item)} +
+ Item {index + 1} + + + + + {#snippet children(field)} + + {#if field.errors} +

{field.errors[0]}

+ {/if} + {/snippet} +
+ + + {#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} +
+``` + +`FieldArray.items` represents the rows currently tracked by the form. When an item is inserted or removed, Formisch updates the related field state along with it. + +The field paths follow the same structure as the schema: + +```javascript +['lineItems', index, 'description'] +``` + +That means the UI structure mirrors the data structure. A row in the interface maps directly to an item in the form state, so validation and updates stay attached to the correct fields. + +To add and remove items: + +```javascript +import { insert, remove } from '@formisch/svelte'; + +function addLineItem() { + insert(invoiceForm, { + path: ['lineItems'], + initialInput: { + description: '', + quantity: '1', + unitPrice: '0' + } + }); +} + +function removeLineItem(index) { + remove(invoiceForm, { + path: ['lineItems'], + at: index + }); +} +``` + +The remove action is disabled when only one item remains because the schema requires at least one line item. The UI behavior and validation rules are coming from the same model. + +--- + +## Step 6: Add reactive totals + +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. + +Instead of updating totals manually inside every input handler, we can derive them from the current form state. + +Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. + +```javascript +import { getInput } from '@formisch/svelte'; + +let invoiceInput = $derived.by(() => { + const taxRate = getInput(invoiceForm, { + path: ['taxRate'] + }); + + const discount = getInput(invoiceForm, { + path: ['discount'] + }); + + const lineItems = getInput(invoiceForm, { + path: ['lineItems'] + }); + + return { + taxRate, + discount, + lineItems + }; +}); + +let totals = $derived(calculateTotals(invoiceInput)); + +function calculateTotals(input) { + const subtotal = input.lineItems.reduce((sum, item) => { + return sum + Number(item.quantity) * Number(item.unitPrice); + }, 0); + + const tax = subtotal * (Number(input.taxRate) / 100); + + const discount = Number(input.discount); + + const total = Math.max(subtotal + tax - discount, 0); + + return { + subtotal, + tax, + discount, + total + }; +} +``` + +`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. + +We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. + +For individual row totals, we can use the same input state: + +```javascript +function getLineTotal(index) { + const item = invoiceInput.lineItems[index]; + + if (!item) return 0; + + return Number(item.quantity) * Number(item.unitPrice); +} +``` + +Because `invoiceInput` is connected to the form, any dependent values update when the form changes. + +--- + +## Step 7: Submit the form + +At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. + +By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. + +```javascript +type InvoiceOutput = v.InferOutput; + +let submittedInvoice = $state(null); + +const submitInvoice = async (output) => { + submittedInvoice = output; + + console.log('Invoice submitted:', output); +}; +``` + +Then connect the submit handler to the form: + +```svelte +
+ +
+``` + +The value received here is the validated schema output, not just the raw values from the inputs. + +That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. + +The form state also exposes submission and validation status, so the UI can react without maintaining separate loading flags. + +To reset the form: + +```javascript +import { reset } from '@formisch/svelte'; + +function resetInvoice() { + submittedInvoice = null; + reset(invoiceForm); +} +``` + +Resetting restores the initial state and clears the form state in one operation. + +--- + +## What we built + +The invoice form now includes: + +- A schema that defines the form structure and validation rules +- Field-level state management through `Field` +- Dynamic collections through `FieldArray` +- Derived values using Svelte 5 reactivity +- Validated and transformed output on submission + +The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. + +Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. + +--- + +## When to use Formisch + +Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. + +If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. + +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. + +This works well when you already have a component system or design language and want the form layer to stay separate from your UI. + +Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. + +Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch. +--- + +## What's next for Formisch + +Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. + +As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. + +For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). + +If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. \ No newline at end of file From 6f1425a2d58f002d1a36e0017452d500e9de6c7f Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 01:59:29 +0000 Subject: [PATCH 2/5] Fix blog post formatting and add missing blog metadata --- .../dynamic-invoice-form-svelte/index.mdx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 61104f9a..a6859919 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -1,12 +1,12 @@ --- +cover: Dynamic invoice form title: 'Building a Dynamic Invoice Form in Svelte 5 with Formisch' description: Build a dynamic invoice form in Svelte 5 using Formisch, Valibot, FieldArray, and typed schema validation. +published: 2026-07-04 authors: - flySewa --- - - # Building a Dynamic Invoice Form in Svelte 5 with Formisch Svelte 5 gives us powerful primitives for managing state with runes, but forms still have their own set of challenges. @@ -40,7 +40,6 @@ We'll be using: You can follow along here, or view the complete example on [Stackblitz](https://stackblitz.com/edit/sveltejs-kit-template-default-vbklpioo?file=src%2Froutes%2F%2Bpage.svelte) ---- ## Step 1: Install dependencies @@ -50,7 +49,6 @@ We'll start by installing Formisch and Valibot: npm install @formisch/svelte valibot ``` ---- ## Step 2: Define the form model @@ -138,7 +136,6 @@ The `lineItems` field is modeled as an array because an invoice can contain a ch This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. ---- ## Step 3: Create the form @@ -181,13 +178,12 @@ const invoiceForm = createForm({ }); ``` -The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. With the form created, the next step is to connect individual fields to that state. ---- ## Step 4: Connect fields @@ -227,7 +223,6 @@ This means the component structure and the data structure stay aligned. The fiel You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. ---- ## Step 5: Add dynamic line items @@ -331,7 +326,6 @@ function removeLineItem(index) { The remove action is disabled when only one item remains because the schema requires at least one line item. The UI behavior and validation rules are coming from the same model. ---- ## Step 6: Add reactive totals @@ -404,7 +398,6 @@ function getLineTotal(index) { Because `invoiceInput` is connected to the form, any dependent values update when the form changes. ---- ## Step 7: Submit the form @@ -466,7 +459,6 @@ function resetInvoice() { Resetting restores the initial state and clears the form state in one operation. ---- ## What we built @@ -482,7 +474,6 @@ The important part is that these pieces are not separate systems. The schema def Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. ---- ## When to use Formisch @@ -497,7 +488,6 @@ This works well when you already have a component system or design language and Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch. ---- ## What's next for Formisch @@ -507,4 +497,4 @@ As we prepare for v1, feedback from developers building real forms is especially For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). -If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. \ No newline at end of file +If you're comparing form libraries, our [comparison guide](https://formisch.dev/svelte/guides/comparison/) looks at how Formisch differs from other approaches in the Svelte ecosystem, including Superforms and TanStack Form. From 16cac43c86c05a8a81425a9bbf6c5052020e16b1 Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 03:12:58 +0100 Subject: [PATCH 3/5] Improve clarity and conciseness in dynamic invoice form guide Refactor content for clarity and conciseness, removing redundant explanations about form model and validation logic. --- .../dynamic-invoice-form-svelte/index.mdx | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index a6859919..b150e1d0 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -54,9 +54,7 @@ npm install @formisch/svelte valibot Before connecting inputs, we need to define what our invoice form looks like. -The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. - -Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. +The form model describes the structure of our data: the fields the user can edit, the validation rules for each field, and the shape of the final output after submission. Instead of defining validation separately and then recreating the same structure as TypeScript types or default values, Formisch can use a schema as the source of truth. Our invoice needs basic metadata, client details, and a list of line items. We'll define those requirements with Valibot: @@ -128,14 +126,9 @@ const InvoiceSchema = v.object({ }); ``` -The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. - -Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. - -The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. - -This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. +The invoice form has fields like `quantity`, `unitPrice`, and `taxRate` that represent numbers in the final invoice data, but the values coming from the inputs are still part of the form's input state. Instead of converting those values in separate event handlers or before submission, we keep that logic with the field definition. The schema handles the transition from input values to the validated data shape we actually want to submit. +The `lineItems` field is modeled as an array because an invoice can contain a changing number of items. Each item has its own fields and validation rules, while the array itself has rules like the minimum and maximum number of items allowed. This keeps the form model as the place where the shape of the data is defined. When a field changes, gets added, or gets removed, the validation rules and submitted output stay connected to that same structure. ## Step 3: Create the form @@ -178,9 +171,7 @@ const invoiceForm = createForm({ }); ``` -The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. - -Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. +The initial values represent the input state of the form, not the final invoice object. That is why numeric values are still strings here — the user is editing input values, and the schema transformation handles converting them when the form is validated. Starting with one `lineItem` also matches the validation rule from the schema. Since the invoice requires at least one item, the form begins in a state that already satisfies that constraint. With the form created, the next step is to connect individual fields to that state. @@ -219,16 +210,11 @@ The `path` tells Formisch where this field exists in the form model. For nested > ``` -This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. - -You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. - +This means the component structure and the data structure stay aligned. The field already knows how to read its value, update the form state, and expose validation results. You don't need separate bindings or error state for every input. Each field remains connected to the form model it belongs to. ## Step 5: Add dynamic line items -Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. - -Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. +Line items are where forms usually become more complex. The number of rows is not fixed, and each row has its own fields and validation state. Instead of manually keeping track of indexes, values, and errors when items are added or removed, Formisch provides `FieldArray` to keep the collection connected to the form model. ```svelte @@ -329,9 +315,7 @@ The remove action is disabled when only one item remains because the schema requ ## Step 6: Add reactive totals -The invoice total depends on several values in the form: quantities, prices, tax, and discounts. - -Instead of updating totals manually inside every input handler, we can derive them from the current form state. +The invoice total depends on several values in the form: quantities, prices, tax, and discounts. Instead of updating totals manually inside every input handler, we can derive them from the current form state. Svelte 5's `$derived` works well here because the calculation automatically stays connected to the values it depends on. @@ -380,9 +364,7 @@ function calculateTotals(input) { } ``` -`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. - -We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. +`getInput` lets us derive only the parts of the form we need. Using `$derived.by()`, the totals stay connected to the relevant fields without recreating the entire form input object whenever an unrelated field changes. We still derive the invoice totals from those values, but the calculation now depends only on the fields it actually uses. That keeps the totals reactive without adding extra synchronization between the form fields and the invoice summary. For individual row totals, we can use the same input state: @@ -401,9 +383,7 @@ Because `invoiceInput` is connected to the form, any dependent values update whe ## Step 7: Submit the form -At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. - -By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. +At this point, the form state, validation, and fields are all connected. The final step is handling the transition from editable input values to the validated invoice data your application can use. By default, Formisch runs validation on submission and provides the parsed output from the schema. This behavior can be configured if your application needs a different validation strategy. ```javascript type InvoiceOutput = v.InferOutput; @@ -440,9 +420,7 @@ Then connect the submit handler to the form: ``` -The value received here is the validated schema output, not just the raw values from the inputs. - -That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. +The value received here is the validated schema output, not just the raw values from the inputs. That means fields like `quantity`, `unitPrice`, `taxRate`, and `discount` have already gone through their transformations. The form moves from input state to application data at the validation boundary. The form state also exposes submission and validation status, so the UI can react without maintaining separate loading flags. @@ -470,20 +448,14 @@ The invoice form now includes: - Derived values using Svelte 5 reactivity - Validated and transformed output on submission -The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. - -Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. +The important part is that these pieces are not separate systems. The schema defines the shape, fields connect the UI, and submission produces the validated result from the same model. Instead of manually syncing inputs, errors, and derived values, the form stays connected throughout its lifecycle. ## When to use Formisch -Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. - -If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. +Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. -It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. - -This works well when you already have a component system or design language and want the form layer to stay separate from your UI. +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. @@ -491,9 +463,7 @@ Native SvelteKit support is also planned, including support for progressively en ## What's next for Formisch -Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. - -As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. +Formisch is currently in [RC](https://formisch.dev/blog/formisch-v1-release-candidate/), with v1 approaching. As we prepare for v1, feedback from developers building real forms is especially useful. If you try Formisch in your own projects, let us know what works well, what feels unclear, and what you'd like improved. For a deeper look at the ideas behind Formisch and how it works internally, you can read the [architecture post](https://formisch.dev/blog/one-core-six-frameworks/). From b587264a9f00d4506823034fec45728aa85c328c Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 19:08:48 +0100 Subject: [PATCH 4/5] Add type annotation to submitInvoice function --- .../routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index b150e1d0..9f41a184 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -390,7 +390,7 @@ type InvoiceOutput = v.InferOutput; let submittedInvoice = $state(null); -const submitInvoice = async (output) => { +const submitInvoice = async (output: InvoiceOutput) => { submittedInvoice = output; console.log('Invoice submitted:', output); From e0088f0a23d91d889bf2e7ea116d6b69d8a4378a Mon Sep 17 00:00:00 2001 From: Oluwawunmi Bewaji Date: Sat, 4 Jul 2026 19:30:10 +0100 Subject: [PATCH 5/5] Enhance Formisch documentation with detailed benefits Expanded the explanation of Formisch to emphasize its schema-native approach, benefits for complex forms, and integration with Svelte 5 reactivity. Clarified its headless nature and future support for SvelteKit. --- .../blog/(posts)/dynamic-invoice-form-svelte/index.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx index 9f41a184..4cdbe397 100644 --- a/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx +++ b/website/src/routes/blog/(posts)/dynamic-invoice-form-svelte/index.mdx @@ -453,11 +453,13 @@ The important part is that these pieces are not separate systems. The schema def ## When to use Formisch -Formisch is a good fit when your form logic mostly lives on the client and the form itself becomes part of your application state. If you have nested data, dynamic fields, complex validation rules, or values that need to update as the user types, keeping the form model connected can remove a lot of manual synchronization. +Formisch is a good fit when your form logic mostly lives on the client and you want a **schema-native** approach to building forms. Instead of defining your data shape, validation rules, transformations, and submitted output separately, the schema becomes the source of truth for the entire form. Fields, validation, and typed output all stay connected to that same model, reducing duplication and the amount of manual synchronization needed as your forms grow. -It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. +This becomes especially useful when working with nested objects, dynamic collections, complex validation rules, or derived values that update as the user types. Rather than wiring those pieces together yourself, they continue to follow the same schema-driven structure. + +Because Formisch is built for Svelte 5, it also fits naturally with rune-based reactivity. Form state can participate in `$derived` calculations and other reactive logic without introducing another state management layer. -Formisch keeps form state, validation, and UI connected, but still gives you full control over your markup. Because of that, it works really well for interactive, browser-first forms in both Svelte and SvelteKit apps. +It is also intentionally headless. Formisch does not provide pre-built inputs or decide how your UI should look. You control the markup and connect it to the form state. This works well when you already have a component system or design language and want the form layer to stay separate from your UI. Native SvelteKit support is also planned, including support for progressively enhanced forms. If your main requirement today is a server-first workflow, a library built specifically for server actions may be a better fit until those capabilities arrive in Formisch.