SvelteKit's form actions let you handle form submissions on the server using plain HTML <form> elements. They work without JavaScript (progressive enhancement) and enhance progressively when JS is available.
Defining an Action
Actions live in +page.server.js alongside your load function:
// src/routes/contact/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const name = data.get('name')?.toString().trim();
const email = data.get('email')?.toString().trim();
const msg = data.get('message')?.toString().trim();
// Server-side validation
if (!name || name.length < 2) {
return fail(400, { name, email, msg, error: 'Name must be at least 2 characters.' });
}
if (!email || !email.includes('@')) {
return fail(400, { name, email, msg, error: 'Valid email required.' });
}
if (!msg || msg.length < 10) {
return fail(400, { name, email, msg, error: 'Message must be at least 10 characters.' });
}
// Save to database, send email, etc.
await sendEmail({ name, email, message: msg });
redirect(303, '/contact/thanks');
}
};The Form Template
<!-- src/routes/contact/+page.svelte -->
<script>
export let form; // contains action return value (errors, data)
</script>
<form method="POST">
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<label>
Name
<input name="name" value={form?.name ?? ''} required>
</label>
<label>
Email
<input name="email" type="email" value={form?.email ?? ''} required>
</label>
<label>
Message
<textarea name="message">{form?.msg ?? ''}</textarea>
</label>
<button type="submit">Send</button>
</form>With no JavaScript, this is a full round-trip: form submits → server validates → redirects or returns errors. It works in any browser, including with JavaScript disabled.
Progressive Enhancement with use:enhance
Add use:enhance to intercept the submission with fetch, keeping the page without a full reload:
<script>
import { enhance } from '$app/forms';
export let form;
let loading = false;
</script>
<form method="POST" use:enhance={()=> {
loading= true;
return async ({ update })=> {
await update();
loading= false;
};
}}>
<button disabled={loading}>
{loading ? 'Sending…' : 'Send'}
</button>
</form>use:enhance is just 1 KB and gives you loading states, optimistic UI, and no page flash — all while falling back gracefully without JS.
Named Actions
One page can have multiple actions:
// +page.server.js
export const actions = {
login: async ({ request }) => { /* ... */ },
register: async ({ request }) => { /* ... */ },
};<!-- two buttons, two actions -->
<form method="POST" action="?/login">
<button>Log in</button>
</form>
<form method="POST" action="?/register">
<button>Register</button>
</form>Live Form with Validation
Cookies and Sessions
Actions can read and set cookies for session management:
export const actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const user = await authenticate(data.get('email'), data.get('password'));
if (!user) return fail(401, { error: 'Invalid credentials' });
cookies.set('session', user.sessionToken, {
path: '/',
maxAge: 60 * 60 * 24 * 7, // 1 week
httpOnly: true,
secure: true,
});
redirect(303, '/dashboard');
}
};