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
  }
};