Skip to content

Forms & Actions

SvelteKit handles form submissions with actions — server functions in +page.server.js that receive the submitted data. A plain HTML <form method="POST"> posts to them, so the feature works before any JavaScript loads.

src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) {
// Return a 400 with the values so the form can re-render the error.
return fail(400, { email, missing: true });
}
const user = await authenticate(email, password);
if (!user) return fail(401, { email, incorrect: true });
cookies.set('session', user.token, { path: '/' });
redirect(303, '/dashboard'); // throw a redirect on success
},
};
src/routes/login/+page.svelte
<script>
let { form } = $props(); // the action's returned value (e.g. fail data)
</script>
<form method="POST">
<input name="email" value={form?.email ?? ''} />
<input name="password" type="password" />
{#if form?.incorrect}<p>Wrong email or password</p>{/if}
<button>Log in</button>
</form>

The action’s return value (including fail(...) data) arrives as the form prop. On success you redirect; on failure you fail(status, data) so the page re-renders with the error and the user’s input preserved.

One page can have several actions — a login form and a register form, say. Name them and target one with action="?/name":

export const actions = {
login: async ({ request }) => { /* … */ },
register: async ({ request }) => { /* … */ },
};
<form method="POST" action="?/login"></form>
<form method="POST" action="?/register"></form>

The form above already works with no JavaScript — the browser posts, the server responds, the page reloads. Add use:enhance from $app/forms and SvelteKit upgrades it to a smooth AJAX submission (no full reload), automatically updating the form prop and invalidating load data — while keeping the no-JS fallback.

<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance> <!-- works without JS, better with it -->
</form>
flowchart TB
  nojs["No JS: browser POSTs the form"] --> server["action runs on the server"]
  js["With use:enhance: AJAX submit, no reload"] --> server
  server --> result["redirect on success, or fail data → form prop"]
Progressive enhancement: works without JS, better with it

This is SvelteKit’s signature move: build with plain forms that work everywhere, then enhance for a better experience — instead of a JS-only form that breaks without it.

Where do SvelteKit form actions live?
How does an action report a validation error while keeping the user's input?
Why do SvelteKit forms work without JavaScript?
What does `use:enhance` (from `$app/forms`) add?