# Send email with Svelte

> Send email from a Svelte app with Email Fast: the browser SDK with a public ef_pub_ key and a server-locked template, or a SvelteKit server route — both shown.

Canonical: https://emailfast.dev/send-email-with/svelte

A Svelte component can send email straight from the browser. Email Fast ships
a browser SDK with EmailJS-compatible endpoints — the recipient always comes from the server-stored template, never from the request, so a public key in your frontend can't be abused to spam arbitrary addresses. For a static Svelte site that is the whole answer; on SvelteKit
you can instead keep a secret key in a server-only form action — both patterns below.

```bash
npm install @email-fast/browser
```

```html
<!-- ContactForm.svelte -->
<script>
  import emailfast from "@email-fast/browser";

  emailfast.init({ publicKey: "ef_pub_..." }); // public by design — safe to ship

  let state = "idle";

  async function submit(event) {
    event.preventDefault();
    state = "sending";
    try {
      await emailfast.sendForm("default", "contact_form", event.target);
      state = "sent";
    } catch {
      state = "error";
    }
  }
</script>

<form on:submit={submit}>
  <input name="name" required />
  <input name="email" type="email" required />
  <textarea name="message" required />
  <button disabled={state === "sending"}>Send</button>
  {#if state === "sent"}<p>Thanks — we got it.</p>{/if}
  {#if state === "error"}<p>That didn't go through. Try again.</p>{/if}
</form>
```

## Or: a SvelteKit form action

On SvelteKit, `$env/static/private` guarantees the key can never reach the client:

```js
// src/routes/contact/+page.server.js
import { EMAILFAST_API_KEY } from "$env/static/private"; // ef_sandbox_… while testing
import { EmailFast } from "@email-fast/nodejs";

const ef = new EmailFast({ apiKey: EMAILFAST_API_KEY, baseUrl: "https://api.emailfast.dev" });

export const actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    await ef.send({
      to: "team@yourdomain.dev",
      subject: "New contact message",
      html: "<p>{{message}}</p>",
      data: { message: String(form.get("message")) },
      idempotency_key: `contact-${form.get("formId")}`, // hidden field set once per render
    });
    return { ok: true };
  },
};
```

## Gotchas

- Never import a secret `ef_live_` key in component code. Vite inlines `VITE_…` vars into the bundle, and SvelteKit refuses `$env/static/private` in client files for exactly this reason — let it.
- The handler calls `event.preventDefault()` itself — Svelte 5 removed the `|preventDefault` modifier, and the explicit call works in every version.
- Form field `name` attributes map to template variables: `name="email"` fills `{{email}}`.
- For the form action, put a `formId` in a hidden input generated when the page renders — a double-submit then replays the same idempotency key instead of sending twice.

## Next

- [Browser quickstart](/docs/quickstart-browser) — templates, `sendForm`, migrating from EmailJS
- [Frontend email without a backend](/features/frontend)
- [The email API](/features/email-api) — for the form-action variant
- [All docs](/docs)
