Contact form for Svelte websites
SvelteKit, classic Svelte, Vite, Astro islands — pick your flavor. One reactive component, full form handling, no backend route. Works with Svelte 4 stores and Svelte 5 runes.
What your Svelte 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 Svelte 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 Svelte 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 Svelte code
Copy the Svelte 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 Svelte form will look like. Submitting opens a confirmation, no real request is sent.
Your Svelte 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
Pick one syntax era: all Svelte 4 OR all Svelte 5 in the same component. Don't mix `let` reactivity with `$state` — it works but is confusing for the next person to touch the file.
- 02
Use `bind:this={formEl}` to get a stable ref to the form element, then call `formEl.reset()` after a successful submit instead of relying on `e.target.reset()` (which can be null after re-renders).
- 03
Set the access key via `VITE_SPLITFORMS_KEY` (Vite) or `PUBLIC_SPLITFORMS_KEY` (SvelteKit). Lock the key to your domain in the splitforms dashboard.
- 04
Add `aria-busy={status === 'loading'}` and `aria-live="polite"` to surface state changes to screen readers — Svelte's reactivity wires this automatically.
- 05
For Svelte islands inside Astro, use `client:visible` so the form doesn't hydrate until the user scrolls to it.
What bites people who skip the docs.
Worth a 60-second skim before you ship to production. Each one has caused a Svelte support ticket at least once.
Svelte 5 runes vs Svelte 4 reactive let — pick one
Svelte 5 introduces $state(...) runes; Svelte 4 uses plain let status = 'idle'. They're not interchangeable in the same component. Check package.json for "svelte": "^5" and use runes accordingly. Mixing them throws a confusing 'rune used outside .svelte.js' error.
on:submit|preventDefault becomes onsubmit={(e) => …} in Svelte 5
If you copy a Svelte 4 snippet into a Svelte 5 project, the on:submit|preventDefault modifier syntax is gone. Use onsubmit={(e) => { e.preventDefault(); … }} or migrate to a SvelteKit form action that handles preventDefault for you.
bind:value desyncs FormData if you forget the `name` attribute
FormData reads from name="…" attributes, not Svelte's bind:value. If you bind a value but skip the name attribute, the field is silently dropped from the POST body. Always set both.
Vite-only Svelte projects can't use $env/static/private
$env/static/private is a SvelteKit feature, not a Vite-Svelte one. In a vanilla Vite + Svelte project, use import.meta.env.VITE_SPLITFORMS_KEY (must have the VITE_ prefix or it's undefined client-side).
Component re-mount on hot-reload resets your status state
During development, saving a .svelte file may re-mount the component mid-submission. The fetch keeps running but the UI loses its 'loading' state. Not a real bug — just don't panic when you see it in dev. Production behaves correctly.
Reactive `$:` block fires before the value you depend on is assigned
If you write $: if (status === 'ok') resetForm(); and status = 'ok' inside an async fetch handler, the block runs synchronously after each top-level update — but resetForm() may execute before the DOM has flushed the disabled state on the submit button, causing a visible flicker where the button briefly re-enables and then the form clears. Either move the reset inside the handler after await tick(), or use Svelte 5's $effect rune which schedules properly relative to renders. Svelte 4's $: is synchronous and easy to misuse for side effects.
How Svelte handles forms without splitforms.
The shape of the problem before splitforms enters the picture — and the gap it fills for Svelte specifically.
Plain Svelte (without SvelteKit) is a compiler — there's no runtime route handler, no server, no built-in form delivery. To ship a working form natively you'd add a separate Node/Bun/Express layer, write the SMTP wiring, and operate it. SvelteKit ships form actions and use:enhance for progressive enhancement, but those just give you ergonomic ways to call your own backend; the actual email-delivery, spam-filtering, and submission-storage are still on you. Svelte 5 runes change reactivity syntax, not the operational model. Splitforms removes the entire 'add a backend' step: the runtime is one URL, hosted by us.
Two ways to ship splitforms on Svelte.
Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.
Pattern A — Svelte 4 reactive let
Classic Svelte syntax: let status = 'idle', reactive by assignment. Works in every Svelte version with a deprecation warning in Svelte 5. Single-file component, no SvelteKit required.
Pattern B — Svelte 5 runes
$state(...) for reactive variables, onsubmit={...} (no on: prefix) for events. Cleaner reactivity model, full TypeScript inference on state.
Shipping Svelte + splitforms to production.
Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.
Vite + Svelte (non-Kit) builds a static bundle for any host. SvelteKit deploys via adapters: @sveltejs/adapter-vercel, -netlify, -cloudflare, -node, -static. The form posts client-side regardless of adapter, so the form itself works identically on each. Use VITE_SPLITFORMS_KEY for plain Vite-Svelte projects; SvelteKit uses $env/static/public from PUBLIC_SPLITFORMS_KEY. Svelte islands embedded in Astro hydrate via client:visible — the form's fetch only runs after the user scrolls to it, saving JS execution on initial load. Lock the access key to your domain.
splitforms vs native svelte.
What you get for free vs what you build, pay for, or do without.
Svelte 5 runes variant (modern syntax)
If your project is on Svelte 5, here's the same form using runes (`$state`) instead of plain `let`. Cleaner reactivity model, no on: prefix.
Things developers ask before they integrate.
Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.
Ship your Svelte 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.
