Get your free splitforms access key
Sign up at splitforms.com, verify your email, and copy your access key from the dashboard. No credit card required.
Contact form · SvelteKit
Use SvelteKit's progressive-enhancement form actions for a no-JS-required experience, or a plain client-side fetch for tight control over loading state. Both patterns work with the splitforms endpoint — standard FormData in, JSON out, every adapter supported.

No server, API route, or SDK. Your SvelteKit form posts straight to one endpoint.
Every submission is emailed to you and saved to a searchable dashboard — spam filtered before it reaches you.
It's your own SvelteKit markup and styles. Splitforms is only the backend, so nothing constrains how the form looks.
Copy-paste ready
Replace YOUR_ACCESS_KEY with the key from your dashboard — that's the whole integration. No SDK to install, no build step, just the svelte you already write.
<!-- src/routes/contact/+page.svelte -->
<script>
let status = "idle";
async function onSubmit(e) {
status = "loading";
const form = e.currentTarget; // capture before the await — currentTarget is null afterwards
const formData = new FormData(form);
formData.append("access_key", "YOUR_ACCESS_KEY");
const res = await fetch("https://splitforms.com/api/submit", {
method: "POST",
body: formData,
});
const data = await res.json();
status = data.success ? "ok" : "err";
if (data.success) form.reset();
}
</script>
<form on:submit|preventDefault={onSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
<button disabled={status === "loading"}>
{status === "loading" ? "Sending…" : "Send"}
</button>
{#if status === "ok"}<p>Thanks! We'll be in touch.</p>{/if}
{#if status === "err"}<p>Something went wrong. Try again?</p>{/if}
</form>How to add it
To add a contact form to a SvelteKit website you need three things: a free splitforms access key, the svelte snippet above, and your key pasted into it. No backend, server, or SDK — the form posts to one URL and every submission lands in your inbox and dashboard.
Sign up at splitforms.com, verify your email, and copy your access key from the dashboard. No credit card required.
Copy the SvelteKit code example into your project and replace YOUR_ACCESS_KEY with the key from step 1.
Submissions arrive in the splitforms dashboard within seconds. Free includes owner email notification attempts; Pro adds Slack, Discord, Sheets, or any signed webhook URL.
Where submissions go
Every submission is emailed to you and saved to a searchable dashboard — spam filtered before it ever reaches you. Search, export to CSV, or forward it to a webhook or Slack on Pro.

No backend needed
Your SvelteKit form posts standard FormData to one URL. Splitforms validates the access key, runs its spam checks, and forwards it to your email — so there's no server, API route, or database for you to build or maintain.

Best practices
The difference between a form that works in the demo and one that survives launch traffic — the production-tested defaults, in priority order.

How SplitForms works
splitforms receives the submission, filters spam, saves it to your dashboard and emails you — then, on paid plans, sends it on to your webhooks and integrations. No backend infrastructure to build.

Point your form to your unique SplitForms endpoint. That's it.

We instantly capture and organize every submission in your inbox.

Send data to email, spreadsheets, CRMs, webhooks, and 7,000+ apps.

No credit card required. Set up in under 60 seconds.
Connect & automate
On paid plans, submissions can also go to Slack, Discord, Google Sheets, Notion, Airtable, Zapier or any signed webhook — and splitforms works on the platforms you build on, from WordPress and Shopify to Next.js. With Stripe connected, your forms can take payments too.

Questions
Two paths. (1) Client-side fetch (snippet above): drop a .svelte file under src/routes/contact/+page.svelte, no server file needed. (2) Form action with progressive enhancement: see the alternative-pattern snippet — adds a +page.server.ts and uses use:enhance.
Yes. The form action just needs to POST FormData to splitforms.com from your +page.server.ts. The alternative-pattern snippet shows the full flow including error handling and redirect.
If using form actions, return fail(400, { message }) from the action — SvelteKit exposes it via the form prop. If using client fetch, render messages from data.message in your status state.
Yes — that's the recommended progressive-enhancement pattern. use:enhance with no arguments uses the default behavior: the form posts traditionally without JS, AJAXes with JS, and the page state updates without a full reload either way.
From a form action: throw redirect(303, '/thanks'). From client fetch: navigate with goto('/thanks'). Or skip both and render an inline success message inside {#if status === 'ok'}.
Yes. Vercel, Netlify, Node, Cloudflare Workers, Cloudflare Pages, static, deno — all of them. The form posts to splitforms.com from the browser, so the adapter only matters for SSR rendering of the form page itself.
If you use +page.server.ts form actions, the form's action attribute must be ?/contact (or whatever you named it) — not /api/contact. Forgetting the ?/ prefix routes to a 404 because SvelteKit doesn't recognize it as an action.
use:enhance without arguments uses default progressive-enhancement behavior — which calls the form action and re-renders. If you need custom logic (toast on error, etc.), return a function: use:enhance={({ formData, cancel }) => async ({ result }) => …}.
If you use a SvelteKit form action that proxies to splitforms.com via fetch, the round-trip eats your CF Worker time budget on cold start. Skip the proxy: have the form POST directly to splitforms.com from the client (the snippet above does this).
$env/static/public is inlined at build time (faster, but key is in the bundle). $env/dynamic/public is read at runtime (slower, but rotatable without rebuild). For the splitforms key, static is fine if you've locked the key to your domain.
If you try to read the splitforms response in +page.server.ts's load(), you'll find it's a separate request. Form action results are passed via the form prop on the page component, not via load. Read them as export let form.
If your +page.server.ts exports actions = { contact: async () => {…}, newsletter: async () => {…} } and your form posts to ?/contact, SvelteKit dispatches by the URL query — but the submitter button must also have formaction="?/contact" if you have multiple submit buttons in one form. Browsers send only the clicked submitter's formaction, so a stray button targeting ?/newsletter will hit the wrong action even when the form's main action is ?/contact. Symptom: submissions inexplicably trigger the wrong handler. Audit every <button type="submit"> for stale formaction attributes left over from layout iteration.
SvelteKit's headline form story is form actions — write a default-exported actions object in +page.server.ts, and SvelteKit handles FormData parsing, progressive enhancement (use:enhance), and result passing via the form prop. It's elegant DX — but it doesn't deliver email. You still write the SMTP integration, the spam-filter logic, the database for storing submissions, the webhook fan-out. The result is a form action that's ~80% boilerplate and ~20% your business logic. Splitforms collapses the boilerplate: the form posts directly from the browser to splitforms.com, the form action becomes a thin proxy (or you skip it entirely), and the operational layer disappears.
SvelteKit's adapter system is the deployment story: @sveltejs/adapter-vercel, -netlify, -cloudflare, -cloudflare-workers, -node, -static. The form's POST is cross-origin to splitforms regardless of adapter. On Cloudflare Pages/Workers (free tier: 10ms CPU per request), avoid Pattern B — the form action's fetch round-trip eats your budget; use Pattern A. On Vercel/Netlify, both patterns work with no measurable difference. $env/static/public inlines at build time (use for client-exposed keys); $env/static/private is server-only (use for Pattern B's server-action key). Domain-lock the access key.
Simple pricing
Free gives you 500 submissions for $0 — no card, no time limit. Pro is $5/month for 5,000 submissions a month, and Annual is $49/year for 15,000 a month with every Pro feature.
For side projects and indie devs.
Every paid feature, for agencies and growing products.
Every Pro feature with triple the volume — $0.27 per 1,000 at full use.
No credit card required on Free • Cancel anytime • Full pricing and FAQ