All resources
Static site forms

How to Handle Form Submissions in SvelteKit

Handle SvelteKit form submissions two ways — form actions with use:enhance, or point a static (adapter-static) site at a form backend with no server code.

The ShipMyForm team

· 4 min read

SvelteKit gives forms first-class treatment. It has a whole primitive built around them — form actions — plus a helper that makes them feel instant without you writing much JavaScript. But there is a catch that trips people up: whether that primitive is even available depends on how you deploy. A SvelteKit site built to run on a server can use form actions; a site built with adapter-static cannot, because there is no server left to run them at request time.

This guide covers both ways to handle a form submission in SvelteKit — native form actions, and pointing the form at a hosted form backend — and where each one fits. If you have read the Astro contact form guide, the static-site half of this will feel familiar: the same "no server, just POST it" approach applies here.

Option A: SvelteKit form actions

This is the idiomatic route when your site runs on a server adapter (adapter-node, adapter-vercel, adapter-cloudflare, and so on). You define an action in +page.server.ts, post a plain <form> to it, and optionally sprinkle on progressive enhancement so it submits without a full page reload.

The server action

Actions live in +page.server.ts next to the route. Each one reads the submitted FormData and returns a result:

ts
// src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get("email");
    const message = data.get("message");

    if (!email || !message) {
      return fail(400, { error: "Email and message are required." });
    }

    // Your code: validate, store, filter spam, send the email…

    return { success: true };
  },
} satisfies Actions;

The form, and progressive enhancement

The markup is a normal <form method="POST">. On its own it works with JavaScript disabled — the browser posts, SvelteKit runs the action, and the page re-renders. Add use:enhance from $app/forms and the same form submits over fetch with no full navigation:

svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  export let form;
</script>

<form method="POST" use:enhance>
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

{#if form?.success}
  <p>Thanks — we&rsquo;ll be in touch.</p>
{/if}

That is the strength of form actions: they are built into the framework, they degrade gracefully, and everything runs in your own code. The cost is that everything runs in your code. Validation, storage, spam filtering, and the actual email all sit inside that action for you to build and maintain — and none of it runs at all unless the deployed site has a server.

Form actions need a server at request time:

Form actions execute in +page.server.ts on each request, so they require a server adapter like adapter-node or adapter-vercel. If you build with adapter-static, there is no server after deploy and the action can never fire — the form just posts into the void. That is exactly the case where a form backend fits.

Option B: point the form at a form backend

If your SvelteKit site is fully static — a marketing site or docs built with adapter-static — or you simply do not want to build and secure backend code for a contact form, point the form at a hosted form backend instead. The endpoint receives the POST, stores the submission, filters spam, and emails you. There is no +page.server.ts and no action to write.

The plain form

Set the form's action to your ShipMyForm endpoint. This is the whole thing, and it works without a line of JavaScript — which means it works on a static build:

svelte
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
  <input name="name" required />
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

By default the browser navigates to a confirmation page after posting. For a static site with no server, that is often all you need.

Adding an inline success message

To keep the visitor on the page, enhance the same form with a small client-side fetch handler. Send the Accept: application/json header so the backend replies with JSON instead of a redirect, then read that response and show a message:

svelte
<script lang="ts">
  let submitted = false;
  let error = "";

  async function handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    const form = event.currentTarget as HTMLFormElement;
    const res = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
      headers: { Accept: "application/json" },
    });
    if (res.ok) {
      submitted = true;
      form.reset();
    } else {
      const body = await res.json().catch(() => ({}));
      error = body.error ?? "Something went wrong. Please try again.";
    }
  }
</script>

{#if submitted}
  <p>Thanks — your message is on its way.</p>
{:else}
  <form
    action="https://shipmyform.com/f/YOUR_FORM_ID"
    method="POST"
    on:submit={handleSubmit}
  >
    <input name="name" required />
    <input name="email" type="email" required />
    <textarea name="message" required></textarea>
    <button type="submit">Send</button>
  </form>
  {#if error}<p>{error}</p>{/if}
{/if}

Because the <form> still has a real action and method, this keeps its progressive enhancement: if JavaScript never loads, the browser posts the plain form and the submission still goes through. You get the inline UX when JS is available and a working fallback when it is not — the same principle behind use:enhance, just pointed at a backend you did not have to build.

Reserved fields you can add:

A few field names get special treatment. Set _replyto to the visitor’s email so replies go straight to them, _subject to control the notification subject line, and add a hidden _gotcha honeypot field — bots fill it in, real people never see it, and ShipMyForm quietly drops anything that trips it.

Form actions vs form backend

Needs a server/adapter?You build validation/spam/storage?Works with adapter-static?Setup
Form actionsYes — adapter-node, adapter-vercel, etc.Yes, all of it in your actionNoWrite +page.server.ts and wire use:enhance
Form backendNoNo — handled for youYesPoint action at your endpoint

Neither is strictly better. If your app already runs on a server and you want the form data flowing through your own logic, form actions are the natural, idiomatic choice. If the site is static, or you just want a contact form that works without standing up and securing a backend, the form backend wins on effort. You can even combine them — let a form backend handle delivery and spam while use:enhance improves the UX.

Getting started with ShipMyForm

Create a form, drop the endpoint into your markup, and you are collecting submissions. Email and webhook connectors are on every plan, and the free tier includes 100 submissions a month — enough for most contact and lead forms before you pay anything.

For the framework-specific details — endpoint setup, JSON responses, and enhancing forms — see the SvelteKit integration guide.

Next steps

Frequently asked questions

Can I use SvelteKit form actions with a static site?
No. Form actions run in +page.server.ts on the server at request time, so they need a server adapter such as adapter-node or adapter-vercel. A site built with adapter-static has no server once it is deployed, so its actions cannot run. For a static build, point your form at a hosted form backend instead — it receives the POST, filters spam, stores the submission, and emails you, with no server code of your own.
How do I add a contact form to a static SvelteKit site?
Write a normal HTML form and set its action to your form backend endpoint, for example https://shipmyform.com/f/YOUR_FORM_ID with method POST. Nothing else is required — the backend handles storage, spam filtering, and email. You can optionally add a small use:enhance or fetch handler to show an inline success message without a full page navigation.
Do I still get progressive enhancement with a form backend?
Yes. Start with a plain HTML form that posts to the backend and works with JavaScript disabled. Then layer on a client-side fetch handler (or SvelteKit's use:enhance) that sends the same request with an Accept: application/json header and shows an inline message. If JS never loads, the plain form still submits.
Form actions or a form backend — which should I use?
If your site already runs on a server adapter and you want to own validation, storage, and email in your own code, form actions are the idiomatic choice. If your site is static (adapter-static) or you just want a contact form working without building and securing a backend, a form backend is the natural fit — and you can combine the two, letting a form backend handle delivery while use:enhance improves the UX.

Related guides