06 - Forms & Actions
Form Actions
Form actions handle form submissions on the server. They work without JavaScript (progressive enhancement) — the form submits as a standard POST request, and SvelteKit handles the rest.
Basic Form Action
Form actions are defined in +page.server.ts and the form is in +page.svelte:
// src/routes/login/+page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email') as string;
const password = data.get('password') as string;
// Process login...
return { success: true }; // returned data is available as `form` in the page
}
};<!-- src/routes/login/+page.svelte -->
<script lang="ts">
let { form } = $props(); // form = return value from the action, null before submission
</script>
<form method="POST">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button>Login</button>
</form>
{#if form?.success}
<p>Login successful!</p>
{/if}Named Actions
When a page has multiple forms, use named actions to distinguish them:
// src/routes/auth/+page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
login: async ({ request }) => {
// Handle login
},
register: async ({ request }) => {
// Handle registration
}
};<!-- src/routes/auth/+page.svelte -->
<!-- action="?/name" targets a specific named action -->
<form method="POST" action="?/login">
<button>Login</button>
</form>
<form method="POST" action="?/register">
<button>Register</button>
</form>Validation with fail()
Use fail() to return validation errors with an HTTP status code. The form data is preserved so the user doesn't lose their input:
// src/routes/contact/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email') as string;
if (!email) {
return fail(400, { email, missing: true }); // 400 status + error data
}
return { success: true };
}
};<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
let { form } = $props();
</script>
<form method="POST">
<input name="email" value={form?.email ?? ''} /> <!-- preserves input on error -->
{#if form?.missing}
<p class="error">Email is required</p>
{/if}
<button>Submit</button>
</form>Progressive Enhancement with use:enhance
By default, forms do a full page reload on submit. Add use:enhance to make them submit via fetch (no reload), with loading states and optimistic UI:
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
let loading = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
await update(); // applies the server response (updates form prop)
loading = false;
};
}}
>
<button disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
</form>Without use:enhance: standard form POST → full page reload.
With use:enhance: fetch request → no reload, form prop updates reactively.
Custom enhance Callback
You can modify form data before sending or handle the result yourself:
<form
method="POST"
use:enhance={({ formData }) => {
formData.append('timestamp', Date.now().toString()); // modify before sending
return async ({ result }) => {
if (result.type === 'success') {
console.log('Success!');
}
};
}}
>
<button>Submit</button>
</form>Redirects After Submission
Use redirect() in an action to send the user to another page after processing:
// src/routes/contact/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
// Process form...
redirect(303, '/success'); // 303 = redirect after POST
}
};