Skip to content
FFormhook
All guides

Guide

SvelteKit contact form (no backend needed)

SvelteKit doesn't need its own form actions endpoint to receive a contact submission. You can drop the same plain HTML form into a +page.svelte, or — if you want inline success/error states — use a small Svelte 5 component with $state and fetch.

Use a plain HTML form

You do not need an API route. The form POSTs directly to Formhook, and your framework never has to touch the submission.

contact.html
<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>

  <label>
    Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: bots fill this; humans don't see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden />

  <!-- optional: send users to a thank-you page after a native POST -->
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />

  <button type="submit">Send</button>
</form>

Or submit with fetch in SvelteKit

Use this version if you want client-side success/error states without a full page reload.

src/routes/contact/+page.svelte
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  let status = $state<"idle" | "sending" | "ok" | "error">("idle");

  async function onSubmit(e: SubmitEvent) {
    e.preventDefault();
    status = "sending";

    const form = e.currentTarget as HTMLFormElement;
    const data = Object.fromEntries(new FormData(form));

    const res = await fetch("https://formhook.app/f/YOUR_API_KEY", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });

    status = res.ok ? "ok" : "error";
    if (res.ok) form.reset();
  }
</script>

{#if status === "ok"}
  <p>Thanks — we'll be in touch.</p>
{:else}
  <form onsubmit={onSubmit}>
    <input type="email" name="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required></textarea>
    <input type="text" name="_gotcha" tabindex="-1" hidden />
    <button type="submit" disabled={status === "sending"}>
      {status === "sending" ? "Sending…" : "Send"}
    </button>
    {#if status === "error"}
      <p>Something went wrong. Please try again.</p>
    {/if}
  </form>
{/if}

How it works

The plain form snippet works in any +page.svelte — no +page.server.ts, no SvelteKit form action handler. If you want client-side UX, the second snippet uses Svelte 5 runes ($state) and posts to formhook.app/f/YOUR_API_KEY directly. The endpoint accepts JSON, form-encoded, or multipart, so Object.fromEntries(new FormData(form)) serialized as JSON is the path of least resistance.

What happens next

  • Submission lands in your Formhook dashboard within a second.
  • Push notification fires on every device you've enabled it on.
  • Reply directly from the dashboard (Pro and above) without standing up your own outbound email.
  • Your SvelteKit deploy stays a static (or near-static) build — no server adapter required for the contact form.

Ship your SvelteKit contact form today.

Sign up free, create a form, paste the API key. Five minutes.

Start free

Free tier: 5 forms · 250 submissions/month · No credit card.

SvelteKit contact form — FAQ

Do I need to write a SvelteKit form action or a +page.server.ts file?
No. The form POSTs to Formhook directly. That keeps your SvelteKit deploy static-friendly — works on Cloudflare Pages, Vercel static, GitHub Pages with adapter-static, anywhere.
How do I handle the success state without a full page reload?
Use the fetch-based snippet. It tracks a $state variable through idle → sending → ok/error and swaps the form for a thank-you message when the POST returns 200. The form is never submitted natively, so the browser doesn't navigate.
Does this work with adapter-static / GitHub Pages?
Yes. Because the submission goes to formhook.app, your SvelteKit site doesn't need a server runtime. Build statically, deploy anywhere.

Guides for other frameworks