integrations · framework

A SvelteKit contact form that survives adapter-static.

SvelteKit has a genuinely good answer for forms: a form action in +page.server.js, progressive enhancement with use:enhance, the whole thing works without JavaScript. It has exactly one catch, and it is the one that bites the people who reach for SvelteKit in the first place. Form actions need somewhere to run. Switch to adapter-static to put your site on GitHub Pages or a CDN and every action in the project stops existing, because there is no server left to receive the POST.

So you either keep a server alive to handle three fields, or you point the form somewhere else. A mailcontact endpoint is somewhere else: submissions land in your project inbox, the build stays fully static, and the contact page behaves the same on adapter-static, adapter-node, or adapter-vercel.

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 SvelteKit.

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/routes/contact/+page.svelte
<script>
  // No +page.server.js. No actions. No adapter requirement.
  let sent = $state(false);

  async function submit(event) {
    await fetch("https://mailcontact.app/api/forms/your-form-id", {
      method: "POST",
      body: new FormData(event.currentTarget),
    });
    sent = 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>
  </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 ready before you touch the routes directory.

02

Add the route

Paste the block into src/routes/contact/+page.svelte. Note what is missing: no sibling +page.server.js, so nothing here forces a server adapter on the rest of your build.

03

Get notified

Wire a Discord, Slack, email, or webhook channel and delivered messages ping you there.

why mailcontact

Built to pair well with SvelteKit.

Your adapter choice stays free

The moment a route needs an action, adapter-static is off the table for the whole project. Keeping the contact form out of the server graph means you can still prerender everything and host it as files.

No server for three fields

adapter-node for a contact page means a process to run, patch, and pay for. That is a lot of infrastructure to justify with a form most sites receive twice a month.

One inbox per side project

SvelteKit is what a lot of people reach for on their fourth idea of the year. Each of those gets its own inbox and its own address rather than all funnelling into your personal mail.

faq

SvelteKit + mailcontact, answered

Why can't I just use a SvelteKit form action?

You can, as long as you keep a server. Actions are server-side handlers, so they need adapter-node, adapter-vercel, or another runtime adapter. With adapter-static there is nothing to POST to and the action is simply not deployed. If your site is otherwise fully static, a contact form is a poor reason to give that up.

Does this work with use:enhance?

use:enhance is built to progressively enhance a SvelteKit action, so it expects an internal action response and does not apply here. The handler above does the equivalent job in about the same number of lines: intercept the submit, post the FormData, swap in your success state.

Can I keep it working without JavaScript?

Yes. Drop the script and give the form a plain action attribute pointing at the endpoint plus method="POST". The browser does a native form post with no client code at all. Today that shows the raw JSON response; a configurable redirect is not currently available.

Does this work in Svelte 4, or only with runes?

Both. The example uses $state and the onsubmit property from Svelte 5, but nothing here depends on runes. On Svelte 4, use let sent = false and on:submit|preventDefault instead; the fetch call is identical.

related

Also works with

One inbox for every SvelteKit 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.