← All guides
React (Vite)

React contact form without a backend

Add a working contact form to a React or Vite app without writing a backend. One fetch() call, JSON response, spam filter without captcha.

A React app built with Vite (or Create React App) ships as static files. There is no server that could receive a form POST or send an email, and putting SMTP credentials into client code is not an option. The usual fix is a hosted form backend: your component posts the form data to an endpoint, the service stores the submission, emails it to you and filters spam. With Formlane that endpoint is https://smallwerk.com/formlane/api/submit. Submissions are stored in Frankfurt and emailed to your account address, with reply-to set to the sender.

Steps

  1. Sign in to Formlane, create a form and copy its access key (fl_…).
  2. Put the key in .env as VITE_FORMLANE_KEY. The key is public by design (it only allows sending to this one form), so it is fine in client code. To be safe, restrict the form to your domain in its settings.
  3. Add the ContactForm component below and render it on your contact page.
  4. Run npm run dev, send a test message and check your inbox and the Formlane dashboard.

Code

.env
VITE_FORMLANE_KEY=fl_YOUR_ACCESS_KEY
src/ContactForm.jsx
import { useState } from "react";

const ENDPOINT = "https://smallwerk.com/formlane/api/submit";

export default function ContactForm() {
  const [status, setStatus] = useState("idle"); // idle | sending | sent | error
  const [error, setError] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();
    setStatus("sending");
    setError("");

    const form = event.currentTarget;
    // FormData skips the unchecked honeypot checkbox, so real visitors never send it.
    const data = Object.fromEntries(new FormData(form));

    try {
      const res = await fetch(ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({
          access_key: import.meta.env.VITE_FORMLANE_KEY,
          subject: "New message from my React site",
          ...data,
        }),
      });
      const json = await res.json();
      if (json.success) {
        setStatus("sent");
        form.reset();
      } else {
        setStatus("error");
        setError(json.message || "Something went wrong.");
      }
    } catch {
      setStatus("error");
      setError("Network error. Please try again.");
    }
  }

  if (status === "sent") return <p role="status">Thanks! Your message has been sent.</p>;

  return (
    <form onSubmit={handleSubmit}>
      {/* Honeypot: hidden from people, leave it unchecked */}
      <input type="checkbox" name="botcheck" style={{ display: "none" }} tabIndex={-1} />

      <label htmlFor="name">Name</label>
      <input id="name" name="name" type="text" required />

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={5} required />

      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" && <p role="alert">{error}</p>}
    </form>
  );
}

The email field becomes the reply-to address of the notification, so you can answer with one click. Using TypeScript? Rename the file to ContactForm.tsx and type the event as React.FormEvent<HTMLFormElement>; nothing else changes.

Without JavaScript handling

If you don't need an inline success message, a plain form works too. The browser posts it and Formlane redirects to your thank-you page:

Plain HTML form
<form action="https://smallwerk.com/formlane/api/submit" method="POST">
  <input type="hidden" name="access_key" value="fl_YOUR_ACCESS_KEY">
  <input type="hidden" name="redirect" value="https://example.com/thanks">
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

What the response looks like

With Accept: application/json Formlane answers with JSON:

  • Success (HTTP 200): {"success": true, "message": "Form submitted successfully", "id": "…"}
  • Error (4xx/5xx): {"success": false, "message": "…", "error": "…"}. Error codes include invalid_key, origin_not_allowed, rate_limited and limit_reached.

Limits: 64 KB per submission, 50 fields, 10,000 characters per field, 10 submissions per 10 minutes per visitor and form. File uploads are not supported yet.

Other ways to do it (honest comparison)

OptionGood forWatch out for
Hosted form backend (Formlane, Formspree, Web3Forms and others)Contact and lead forms on static React/Vite apps; no server to runMonthly submission quotas; each service has its own free tier and feature set (file uploads, webhooks, autoresponders differ)
Email-from-the-browser services (e.g. EmailJS)Sending an email straight from client code with a templateMessages are emails, not a stored submission list; check how spam is handled
Your own serverless function (Vercel/Netlify/Cloudflare function + an email API)Full control, custom logicYou maintain code, secrets, spam protection and email deliverability yourself
Netlify FormsSites deployed on NetlifyForms are detected at deploy time, so it only works for sites hosted on Netlify

Formlane is the right pick if you want a simple endpoint, submissions stored in Frankfurt and a spam filter without captcha. If you need file uploads, autoresponders or webhooks today, choose a service that already has them: Formlane doesn't yet.

Pricing

Free: 250 submissions a month and up to 5 forms. Starter: $8/month for 2,500 submissions. Pro: $19/month for 25,000 submissions. Prices are in USD, plus applicable taxes.

FAQ

Can I send email from React without a backend?

Not directly: a browser can't talk to an SMTP server, and SMTP credentials must never ship in client code. A form backend receives the data and sends the email for you.

Is it safe to put the access key in my React code?

Yes. The Formlane access key is public by design and only allows sending submissions to that one form. Restrict the form to your domain in its settings so other sites can't use it.

Does this work with Vite, Create React App and React Router?

Yes. The component only uses fetch(), so it works in any client-side React setup. With Vite, environment variables must start with VITE_.

How is spam filtered?

A hidden honeypot checkbox, a rate limit per visitor and simple content rules (for example link flooding). Flagged messages go to a separate Spam tab and are not emailed. No captcha and no third-party scripts.

Where are submissions stored?

In a database in Frankfurt (Supabase, region eu-central-1). The app runs on Vercel and notifications are sent via an email provider; the privacy policy lists all providers.

What happens when I reach the monthly limit?

New submissions are rejected with the error limit_reached until the next calendar month or an upgrade. Stored submissions stay.

Get your access key

Free for 250 submissions a month. Submissions stored in Frankfurt.

Get your access key