---
title: 'Progressively enhancing forms with enhance'
slug: progressively-enhancing-forms-with-enhance
description: 'Progressively enhance HTML forms to submit via fetch when JavaScript is available.'
---
## Progressively enhancing forms with enhance
`enhance` is a Svelte attachment that progressively enhances `
```
Place the form inside a hydrated island (`mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer mochi:hydrate`). When hydration is skipped the attachment never runs and the form falls back to a native HTML POST — that is the progressive-enhancement contract.
Do **NOT** call `enhance()` outside a hydrated island; instead, mark the surrounding component with `mochi:hydrate*` so the attachment can attach in the browser.
`enhance` is a factory: call it as `{@attach enhance()}` even with no options. Attachments require Svelte 5.29+.
### Wire format
`enhance` adds `Accept: application/json` and `x-mochi-action: true` to the POST. The server detects either header and responds with one of four shapes:
```ts
type MochiEnhanceResult =
| { type: 'success'; status: number; data?: unknown }
| { type: 'failure'; status: number; data?: unknown }
| { type: 'redirect'; status: number; location: string }
| { type: 'error'; status?: number; error: unknown };
```
HTTP status is `200` for `success`, `failure`, and `redirect`; the body's `status` field carries the action's status. For `error`, the HTTP status matches the error code. `data` is encoded with [devalue](https://www.npmjs.com/package/devalue) so `Date`, `Map`, `Set`, `BigInt`, and cyclic references survive the wire.
### Default fallback
Without a callback, `enhance` runs a minimal default per result type:
| `result.type` | Default |
| ------------- | ------------------------------------------------- |
| `success` | `form.reset()` |
| `failure` | nothing (provide a callback to update the UI) |
| `redirect` | `window.location.assign(result.location)` |
| `error` | `console.error('[mochi] enhance:', result.error)` |
**The default fallback is intentionally lean.** Mochi has no client-side `page.form` store, no `goto`, and no `invalidateAll` — the framework cannot auto-update component props or re-run server data after a submission. Pass a `submit` callback to react to `failure` or to do anything beyond a redirect.
When the same component renders both as a hydrated island (where `enhance` will fire) and as a plain SSR-only child (where it won't), read the [auto-injected `isHydratable` prop](/docs/environment-constants/#auto-injected-props) to skip the SSR `form`-prop peek when the client will take over.
### Submit callback
Pass a function as the argument. It runs once per submit and may return a result handler that fully replaces the default fallback:
```svelte
```
The result handler receives `{ result, formElement, formData, action, update }`. Call `update({ reset?: boolean })` to re-invoke the default fallback — useful when layering extra behavior on top of it.
### onPending
Pass an options object with `onPending` instead of tracking a `pending` flag inside the submit function. It fires `true` immediately before the fetch and `false` once the result handler settles (or when the submission is cancelled):
```svelte
```
`onPending` fires `false` from a `finally` block, so it resets even if the fetch throws or the abort signal fires.
### Cancelling
The `submit` callback receives `cancel` and `controller`:
- `cancel()` — bail out before `fetch` is issued. No callback runs.
- `controller.abort()` — cancel an in-flight request. The `AbortError` is silently swallowed.
### Server-side
No server changes are needed beyond declaring the action. The same `Mochi.page(path, { actions })` definition serves both the no-JS HTML POST flow and the enhanced JSON flow:
```ts
// file: src/index.ts
import { Mochi, fail, success } from 'mochi-framework';
await Mochi.serve({
routes: {
'/login': Mochi.page('./src/Login.svelte', {
actions: {
default: ({ formData }) => {
const username = String(formData.get('username') ?? '');
if (!username) return fail(400, { error: 'Username required' });
return success({ username });
},
},
}),
},
});
```
Returning a `Response` directly from an action bypasses the JSON envelope on enhanced submissions — treat that path as an escape hatch.
**Wrap data in `success()` to round-trip it to the client.** A plain return like `return { username }` strips the data on the enhanced path and the result handler receives an empty `data` object. This matches the non-enhanced behavior. Always use `success()` when the client needs the returned data.
Do **NOT** return a plain object from an action when the enhanced client needs `data`; instead, wrap it with `success()` (or `fail()` for errors).
### deserialize
`deserialize(text)` decodes a raw `MochiEnhanceResult` envelope. Use it when rolling your own `onsubmit` instead of `{@attach enhance(...)}`:
```svelte
```
### When to use enhance
Reach for `enhance` when the action's outcome should update UI without a navigation flicker — interactive forms, optimistic patterns, inline validation. Stick with a plain `