Limited-time offer The first 2,000 users get 10,000 emails/month free for a year 1,995 of 2,000 spots left Claim yours

Guides

Svelte & SvelteKit contact form that sends email

Two ways to send email from Svelte: a client-side component that works on any static build, or a SvelteKit form action that keeps your key secret.

Last updated October 26, 2026

1Create a FormSMTP project

Sign up free and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click Test connection. Not sure which provider? Your existing mailbox works: see the Gmail or Zoho guides.

2Lock the project to your site

In Settings → Security, set Allowed websites to https://yoursite.com and Allowed recipients to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.

3Option A: Client-side component

Works with adapter-static, plain Svelte on Vite, or any host. Add PUBLIC_FORMSMTP_KEY=… to .env.

svelte
<!-- src/lib/ContactForm.svelte -->
<script>
  import { PUBLIC_FORMSMTP_KEY } from "$env/static/public";

  let status = $state("idle"); // idle | sending | sent | error
  let error = $state("");

  async function onsubmit(e) {
    e.preventDefault();
    status = "sending";
    try {
      const res = await fetch("https://app.formsmtp.com/api/v1/form/" + PUBLIC_FORMSMTP_KEY, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new URLSearchParams(new FormData(e.currentTarget)),
      });
      const data = await res.json();
      if (!data.success) throw new Error(data.error);
      status = "sent";
    } catch (err) {
      error = err.message;
      status = "error";
    }
  }
</script>

{#if status === "sent"}
  <p>Thanks! We'll be in touch soon.</p>
{:else}
  <form {onsubmit}>
    <input name="name" placeholder="Name" required />
    <input name="email" type="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required></textarea>
    <input name="_gotcha" style="display: none" tabindex="-1" autocomplete="off" />
    <button disabled={status === "sending"}>{status === "sending" ? "Sending…" : "Send"}</button>
    {#if status === "error"}<p role="alert">{error}</p>{/if}
  </form>
{/if}

Not using SvelteKit? Replace the $env import with import.meta.env.VITE_FORMSMTP_KEY. On Svelte 4, use let variables and on:submit|preventDefault.

4Option B: SvelteKit form action

When your app runs on a server (Node, Vercel, Netlify, Cloudflare), keep the key private. Add FORMSMTP_KEY=… to .env and leave Allowed websites empty, since requests come from your server.

js
// src/routes/contact/+page.server.js
import { fail } from "@sveltejs/kit";
import { FORMSMTP_KEY } from "$env/static/private";

export const actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    const res = await fetch("https://app.formsmtp.com/api/v1/send", {
      method: "POST",
      headers: { Authorization: `Bearer ${FORMSMTP_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        to: "you@yourdomain.com",
        replyTo: form.get("email"),
        subject: `New message from ${form.get("name")}`,
        text: String(form.get("message")),
      }),
    });
    const data = await res.json();
    if (!data.success) return fail(res.status, { error: data.error });
    return { sent: true };
  },
};
svelte
<!-- src/routes/contact/+page.svelte -->
<script>
  import { enhance } from "$app/forms";
  let { form } = $props();
</script>

{#if form?.sent}
  <p>Thanks! We'll be in touch soon.</p>
{:else}
  <form method="POST" use:enhance>
    <input name="name" placeholder="Name" required />
    <input name="email" type="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required></textarea>
    <button>Send</button>
    {#if form?.error}<p role="alert">{form.error}</p>{/if}
  </form>
{/if}

With use:enhance the page doesn’t reload; without JavaScript it still works as a normal form post.

FAQ

Which option should I use?

Use the form action whenever your SvelteKit app runs on a server, since the key never reaches the browser. Use the client-side component for adapter-static builds and plain Svelte.

Why does $env/static/public need the PUBLIC_ prefix?

SvelteKit only lets variables starting with PUBLIC_ into browser code, so private secrets can’t leak by accident. A FormSMTP key locked to your site is fine to make public.

Why did my message land in spam?

Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.

More guides

Get a Founding Pass

10,000 emails a month, free for your first year. Pass No. 0006 is next: 1,995 of 2,000 left, no card.