Skip to content
FFormhook
All guides

Guide

Gatsby contact form (no backend needed)

Gatsby builds static HTML, so a Gatsby contact form is just plain HTML at runtime. Drop in the form action, or use a small React component with fetch if you want inline success and error states.

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 Gatsby

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

src/components/contact-form.tsx
// src/components/contact-form.tsx
import { useState } from "react";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");

    const form = e.currentTarget;
    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),
    });

    setStatus(res.ok ? "ok" : "error");
    if (res.ok) form.reset();
  }

  if (status === "ok") return <p>Thanks — we'll be in touch.</p>;

  return (
    <form onSubmit={onSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <input type="text" name="_gotcha" tabIndex={-1} hidden />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" && <p>Something went wrong. Please try again.</p>}
    </form>
  );
}

How it works

The plain form snippet can go in any Gatsby page or component — it's pure HTML, no Gatsby-specific data layer needed. The fetch-based React component handles the submission client-side and posts to formhook.app/f/YOUR_API_KEY. No gatsby-source-formhook plugin to install, no serverless function to deploy.

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) — keeps your real email out of your Gatsby site's static HTML.

Ship your Gatsby 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.

Gatsby contact form — FAQ

Do I need a Gatsby Function or a Netlify function for this?
No. The form POSTs to Formhook directly. Your Gatsby site can stay a pure static build — no serverless function adapter required.
Can I use this with `gatsby develop` locally?
Yes. The browser makes the POST regardless of how Gatsby served the HTML. Just make sure http://localhost:8000 is on the form's allowed-origins list while you develop.
How do I handle spam without adding a Gatsby plugin?
You don't need to. The honeypot field drops bots silently, and Cloudflare Turnstile is a per-form toggle in the Formhook dashboard.

Guides for other frameworks