integrations · framework

A Svelte contact form without a backend.

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

Use the MailContact script with your own design.

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.

contact.html
<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

Alternative: a direct integration for Svelte.

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.

src/lib/ContactForm.svelte
<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

Three steps, start to finish.

01

Create an inbox

One inbox per project, with a form endpoint and a project email address available right away.

02

Add the component

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.

03

Get notified

Add a Discord, Slack, email, or webhook channel and the message reaches you as it lands.

why mailcontact

Built to pair well with Svelte.

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.

No state to declare

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.

It embeds anywhere

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

Svelte + mailcontact, answered

What's the difference between this and the SvelteKit setup?

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.

Can I use this with Svelte 4?

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.

Is my endpoint id a secret?

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.

Can I send this to Discord instead of email?

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

Also works with

One inbox for every Svelte project you ship.

Forms and a real email address, together, with notifications where you live. Start with a free account.

Create your free form

No credit card needed. Free includes one project and 50 form messages per month.