Contact form for SvelteKit apps
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.
What your SvelteKit contact form actually looks like.
Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your SvelteKit project — no SDK, no plugin, no PHP.
- ✓1,000 submissions per month, free forever
- ✓Honeypot + AI spam classifier on every plan
- ✓Signed webhooks to Slack, Discord, your server
Ship a SvelteKit contact form without a backend.
No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.
Get your free access key
Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.
By signing up, you agree to our terms and privacy policy.
Drop in the SvelteKit code
Copy the SvelteKit snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.
Submissions land in your inbox
Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.
Try it now — no signup, no key.
This is a styled HTML preview of what your SvelteKit form will look like. Submitting opens a confirmation, no real request is sent.
Your SvelteKit form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.
- →14ms median round-trip from the edge.
- →Honeypot + classifier, no CAPTCHA.
- →Per-domain key locking out of the box.
{
"access_key": "sk_live_4f9a_••••",
"name": "Maya Iyer",
"email": "maya@studio71.co",
"message": "…"
}How to ship this without regrets.
Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.
- 01
Default to the client-side fetch pattern (the snippet above) — fewer moving parts, works on every adapter without a server route.
- 02
If you need progressive enhancement (form works without JS), use form actions with `use:enhance` — the form posts traditionally on JS-disabled clients and AJAXes when JS loads.
- 03
Set the access key as `PUBLIC_SPLITFORMS_KEY` in `.env` — accessible from `$env/static/public`. Lock the key to your domain in the splitforms dashboard.
- 04
After successful submit, navigate via `goto('/thanks', { invalidateAll: true })` rather than just resetting the form — gives the user clear feedback that something happened.
- 05
For Cloudflare Workers / Pages adapter, never proxy through your server route. Direct browser → splitforms POST keeps you under the CPU time budget.
What bites people who skip the docs.
Worth a 60-second skim before you ship to production. Each one has caused a SvelteKit support ticket at least once.
Form actions need POST and the named action prefix
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 disables your client-side handler if you don't return a callback
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 }) => …}.
Cloudflare adapter has a 50ms cold start budget — fetch to splitforms eats it
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 vs $env/dynamic/public — pick the right one
$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.
load() functions can't access the form action result
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.
Named form actions need explicit `name` matching, not just URL hints
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.
How SvelteKit handles forms without splitforms.
The shape of the problem before splitforms enters the picture — and the gap it fills for SvelteKit specifically.
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.
Two ways to ship splitforms on SvelteKit.
Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.
Pattern A — client-side fetch (skip form actions)
Pure +page.svelte with a fetch handler — no +page.server.ts needed. Simpler, works on every adapter without server CPU time, no proxying. Best for Cloudflare Workers / Pages where CPU budget matters.
Pattern B — form action with use:enhance (no-JS support)
Server-side form action proxies to splitforms; key stays in $env/static/private. With use:enhance, the form posts traditionally without JS (full-page reload, splitforms 302 to /thanks) and AJAXes when JS loads. Maximum compatibility, slight CPU cost on the server.
Shipping SvelteKit + splitforms to production.
Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.
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.
splitforms vs native sveltekit.
What you get for free vs what you build, pay for, or do without.
Form actions variant — progressive enhancement, no client JS required
If you want the form to work without JavaScript (and progressively enhance with `use:enhance`), use a SvelteKit form action that proxies to splitforms server-side. Hides the access key entirely.
Things developers ask before they integrate.
Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.
Ship your SvelteKit contact form in 60 seconds.
1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.
