The bundle stays a bundle
A Svelte app compiled with Vite is a folder of files you can drop on any static host. Adding a form service to it would normally mean adding a server; this way the artifact you deploy does not change shape.
integrations · framework
Plain Svelte, without SvelteKit, compiles to a bundle of JavaScript that runs in the browser and nothing else. There is no server in the picture at all, which is exactly why people pick it for a widget, a landing page, or an island inside someone else's site. It also means a contact form has nowhere to send anything.
The usual fix is to introduce a backend, at which point you are maintaining a service for the sake of three fields. Pointing the form at a mailcontact endpoint keeps the deployment as static as it was: submissions land in your project inbox, with a ping in Discord, Slack, email, or a webhook.
the snippet
Point the form at your project's mailcontact endpoint. Submissions land in your project inbox. No server, no API route, no email service to wire up.
<script>
let sent = $state(false);
let error = $state(false);
async function submit(event) {
const res = await fetch("https://mailcontact.app/api/forms/your-form-id", {
method: "POST",
body: new FormData(event.currentTarget),
});
if (res.ok) sent = true;
else error = true;
}
</script>
{#if sent}
<p>Thanks. We'll be in touch.</p>
{:else}
<form onsubmit={(e) => { e.preventDefault(); submit(e); }}>
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="you@email.com" required />
<textarea name="message" placeholder="What's on your mind?" required></textarea>
<input name="_gotcha" tabindex="-1" autocomplete="off" hidden />
<button>Send</button>
{#if error}<p role="alert">That didn't send. Try again?</p>{/if}
</form>
{/if}how it works
One inbox per project, with a form endpoint and a project email address available right away.
Paste it anywhere in your component tree and swap the endpoint id. It reads the form element on submit, so the name attributes in the markup are the fields that arrive in your inbox.
Add a Discord, Slack, email, or webhook channel and the message reaches you as it lands.
why mailcontact
A Svelte app compiled with Vite is a folder of files you can drop on any static host. Adding a form service to it would normally mean adding a server; this way the artifact you deploy does not change shape.
Reading new FormData(event.currentTarget) means there is nothing to bind. No value per input, no reset after submit, and the browser's own required and type checks still fire before your handler runs.
Svelte components get compiled into other people's pages all the time. Because the endpoint accepts cross-origin posts, the form works from whatever domain the component ends up embedded on.
faq
SvelteKit has form actions, which are real server handlers, so the question there is whether you keep an adapter that can run them. Plain Svelte has no server story at all, so there is nothing to weigh: the form has to post somewhere external no matter what. If you are on SvelteKit, read the SvelteKit guide instead.
Yes. The example uses runes ($state) and Svelte 5's onsubmit property. On Svelte 4, use let sent = false with on:submit|preventDefault={submit}. The fetch and the FormData are unchanged, since neither is a framework feature.
No, and it is not treated as one. It goes in your public markup by design, the same way a Google Form URL does. It only accepts submissions into one inbox: there is nothing to read back with it, so exposure costs you nothing but the odd spam post, which the honeypot handles.
Yes, and you should not do it with a webhook in your Svelte code. A Discord webhook URL pasted into a component ships to every visitor's browser, and anyone who reads it can post to your channel forever. Add Discord as a notification channel on the inbox instead: the relay happens server-side and the URL never leaves it.
related
Forms and a real email address, together, with notifications where you live. Launching soon. Be the first to know.