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.
recommended setup
Add this HTML to your site, or use the form builder. Replace the form link with yours. The hosted script injects the hidden check, handles sending and automatic verification, and preserves the draft on errors. You control the HTML and CSS. Free forms display a visible “Processed by mailcontact.app” link; Pro and Plus do not. Feedback follows the browser’s language; set data-mailcontact-language="fr" on the form to force French.
<form action="https://mailcontact.app/api/forms/your-form-id" method="POST" data-mailcontact-form>
<fieldset>
<legend>Contact us</legend>
<label>
Name (optional)
<input name="name" type="text" autocomplete="name" maxlength="200" />
</label>
<label>
Email
<input name="email" type="email" autocomplete="email" maxlength="320" required />
</label>
<label>
Message
<textarea name="message" rows="5" maxlength="5000" required></textarea>
</label>
<noscript>
<div aria-hidden="true" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;">
<label>Website
<input name="company_website" type="text" tabindex="-1" autocomplete="off" />
</label>
</div>
</noscript>
<button type="submit">Send message</button>
</fieldset>
<p data-contact-status role="status" aria-live="polite" aria-atomic="true"></p>
</form>
<script defer src="https://mailcontact.app/forms/v1.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>For a component with its own submit handler, load the same script and use MailContact.prepare(form) and MailContact.send() instead of the automatic marker. See the custom interface reference for loading, feedback and Free attribution.
the snippet
This example posts directly to the API. Keep the required company_website field present and empty. Native posts open JSON on success or a verification page when needed. Custom scripts without verification support send suspicious messages to Spam for review; strong spam is also held there without notifications or quota usage. A success response confirms reception, including review, or a silent discard of a filled check. See spam protection for the full behavior.
<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="[email protected]" required />
<textarea name="message" placeholder="What's on your mind?" required></textarea>
<input name="company_website" type="text" tabindex="-1" autocomplete="off"
style="position:absolute;left:-10000px" aria-hidden="true" />
<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 server-side limits, content checks and campaign detection protect reception. The hidden check catches some basic bots; suspicious messages can require verification or go to Spam for review.
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. Start with a free account.
Create your free formNo credit card needed. Free includes one project and 50 form messages per month.