Skip to main content
Svelte & SvelteKit·Lesson 4 of 5

Forms & Actions

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

Ctrl+Enter
HTML
CSS
JS
Preview

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