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.
integrations · framework
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.
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>
// 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="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>
</form>
{/if}how it works
One inbox per project, with a form endpoint and a project email address ready before you touch the routes directory.
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.
Wire a Discord, Slack, email, or webhook channel and every submission pings you there.
why mailcontact
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.
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.
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
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.
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.
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 coming with the launch.
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
Forms and a real email address, together, with notifications where you live. Launching soon. Be the first to know.