06 - Forms & Actions

📋 Jump to Takeaways

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 instead — no reload, and the form prop updates automatically.

With no callback, use:enhance handles everything for you. Add a callback only when you need custom behavior (loading states, modifying data, etc.):

<!-- Simple — no callback needed for basic enhancement -->
<form method="POST" use:enhance>
  <button>Submit</button>
</form>

<!-- Custom — add a callback for loading states or custom logic -->
<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
  }
};

Key Takeaways

  • Form actions are defined in +page.server.ts via export const actions: Actions and handle method="POST" submissions on the server
  • The default action handles a single form; named actions (action="?/login", action="?/register") distinguish multiple forms on one page
  • Access form data with const data = await request.formData() and individual fields with data.get('fieldName')
  • Return validation errors with fail(400, { field, errorFlag: true }) — this preserves user input and sets the form prop
  • The form prop (let { form } = $props()) holds the action's return value; it's null before any submission
  • Add use:enhance to a form for fetch-based submission with no page reload; without it, forms do a full POST and reload
  • Customize use:enhance with a callback to add loading states or modify formData before sending
  • Redirect after form processing with redirect(303, '/destination') from @sveltejs/kit

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy