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:
// 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:
<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’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 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:
<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:
<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.
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 actions | Yes — adapter-node, adapter-vercel, etc. | Yes, all of it in your action | No | Write +page.server.ts and wire use:enhance |
| Form backend | No | No — handled for you | Yes | Point 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
- New to the idea? Start with what a form backend actually is.
- Building on Astro too? See the Astro contact form guide.
- Want the raw email route? Read how to send an HTML form to your email.
- Worried about bots? Learn to stop form spam without reCAPTCHA.
- Start free — no card required, 100 submissions a month, email and webhook connectors on every plan.
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
Astro Contact Form Without a Backend (2026)
A copy-paste contact form for Astro that emails you on every submission — no server or API route.
What Is a Form Backend? (And When You Need One)
The plain-language definition, how form endpoints work, and when a hosted backend beats rolling your own.
How to Send an HTML Form to Your Email (Without a Backend)
Why mailto: and PHP mail() let you down on a static site — and the reliable way to get form submissions into your inbox, spam folder avoided.