How to Build a Gatsby Contact Form

Last updated: March 2026

Gatsby is a React-based static site generator that builds blazing-fast websites from your components and data sources. Because Gatsby produces static HTML at build time, there's no built-in server to handle form submissions. If you want a contact form that actually works, you need an external backend to receive the data, send email notifications, and store submissions.

This guide walks you through adding a fully functional contact form to a Gatsby site using FormWit as the form backend. No server code, no Gatsby plugins, no Lambda functions. Just a React component that sends data to an endpoint.

How it works

Gatsby generates static files at build time, so your site runs without a server in production. When a visitor fills out your contact form, the browser sends the form data directly to FormWit's endpoint via a POST request. FormWit validates the data, checks for spam, stores the submission in your dashboard, and sends you an email notification.

The complete flow:

  1. A visitor fills out the contact form on your Gatsby site
  2. Your React component sends the form data to your FormWit endpoint using fetch
  3. FormWit validates the data and filters out spam
  4. The submission is stored in your dashboard and you receive an email notification
  5. Your visitor sees a success message without leaving the page

This approach works because the form submission happens entirely in the browser at runtime. It has nothing to do with Gatsby's build process or static generation.

Set up a Gatsby contact form

Step 1: Create a FormWit account

Go to app.formwit.com/auth/signup and create a free account. No credit card required. Once signed in, click Create Form and give it a name (e.g., "Gatsby Contact Form"). You'll receive a unique endpoint URL that looks like https://app.formwit.com/api/s/YOUR_FORM_ID. Copy this. You'll need it in the next step.

Step 2: Create the ContactForm component

Create a new file at src/components/ContactForm.jsx in your Gatsby project. This component handles form submission via fetch, tracks submission status with React's useState hook, and includes a honeypot field for spam protection.

import React, { useState } from 'react';

function ContactForm() {
  const [status, setStatus] = useState('idle');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');

    const form = e.target;
    const data = new FormData(form);

    try {
      const response = await fetch(form.action, {
        method: 'POST',
        body: data,
      });

      if (response.ok) {
        setStatus('success');
        form.reset();
      } else {
        setStatus('error');
      }
    } catch {
      setStatus('error');
    }
  }

  return (
    <form
      action="https://app.formwit.com/api/s/YOUR_FORM_ID"
      method="POST"
      onSubmit={handleSubmit}
    >
      <div>
        <label htmlFor="name">Name</label>
        <input type="text" id="name" name="name" required />
      </div>

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

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

      {/* Honeypot field for spam protection */}
      <input
        type="text"
        name="_gotcha"
        style={{ display: 'none' }}
        tabIndex={-1}
        autoComplete="off"
      />

      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending...' : 'Send Message'}
      </button>

      {status === 'success' && (
        <p style={{ color: 'green' }}>Message sent successfully!</p>
      )}
      {status === 'error' && (
        <p style={{ color: 'red' }}>Something went wrong. Please try again.</p>
      )}
    </form>
  );
}

export default ContactForm;

Replace YOUR_FORM_ID with the endpoint ID from your FormWit dashboard. The component uses FormData to serialize the form fields and sends them via fetch. The hidden _gotcha field is a honeypot. Bots fill it in automatically, but real visitors never see it, so FormWit uses it to filter spam. For more on how this works, see the spam protection guide.

Step 3: Add the component to a Gatsby page

Import the ContactForm component into a page file. Gatsby uses file-based routing, so creating src/pages/contact.js will automatically create a /contact route on your site.

import React from 'react';
import ContactForm from '../components/ContactForm';

function ContactPage() {
  return (
    <main>
      <h1>Contact Us</h1>
      <p>Have a question or want to get in touch? Fill out the form below.</p>
      <ContactForm />
    </main>
  );
}

export default ContactPage;

export function Head() {
  return <title>Contact Us</title>;
}

This uses Gatsby's Head export (available in Gatsby 4.19+) to set the page title. If you're using an older version, use gatsby-plugin-react-helmet or Gatsby's Seo component instead.

Step 4: Test the form

Start the Gatsby development server:

gatsby develop

Navigate to http://localhost:8000/contact, fill out the form, and click "Send Message." You should see the success message appear without a page reload. Check your FormWit dashboard (the submission will appear there) and check your email inbox for the notification.

Once you confirm it works in development, run gatsby build && gatsby serve to verify it also works with the production build. The form sends data at runtime (in the browser), so it works identically in development and production.

Gatsby-specific details

Works with file-based and programmatic pages

The ContactForm component works anywhere in your Gatsby project. You can import it into file-based pages in src/pages/, use it in programmatic pages created via createPage in gatsby-node.js, or embed it in any layout or template component. Since the form submission happens entirely in the browser, it doesn't interact with Gatsby's build pipeline at all.

No Gatsby plugin needed

Unlike some form solutions that require a dedicated Gatsby plugin for build-time configuration, FormWit works with plain React components. There's nothing to add to your gatsby-config.js and no plugin API to learn. The component is just standard React with a fetch call. Gatsby treats it like any other component.

Compatible with gatsby-plugin-offline

If you use gatsby-plugin-offline to make your site work as a Progressive Web App, form submissions still work normally. The service worker caches your static pages, but outgoing fetch requests (like the POST to FormWit) pass through to the network. When the visitor is online, submissions are sent immediately. Note that submissions made while fully offline will fail. If offline support is critical, you could add retry logic to the component's error handler.

Works with Gatsby's Head API and SEO components

The contact form component is purely a body-level element, so it doesn't conflict with Gatsby's Head API, gatsby-plugin-react-helmet, or any SEO component you use. You can set meta tags, Open Graph data, and structured data on your contact page independently of the form functionality.

Adding a custom redirect

If you prefer to redirect visitors to a thank-you page after submission instead of showing an inline success message, add a hidden redirect_to field:

<input type="hidden" name="redirect_to" value="https://yoursite.com/thank-you" />

Since the component above handles submission via fetch (which prevents the default browser redirect), you'll need to handle the redirect in JavaScript instead:

if (response.ok) {
  window.location.href = '/thank-you';
}

You can create a src/pages/thank-you.js page in Gatsby for this, keeping everything within your static site.

Styling the form

The component above uses no CSS so it fits into any styling approach. Gatsby supports CSS Modules, styled-components, Emotion, Tailwind CSS, and plain CSS out of the box. Here's a quick example using CSS Modules:

Create src/components/ContactForm.module.css:

.form {
  max-width: 500px;
}

.field {
  margin-bottom: 1rem;
}

.field label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 600;
}

.field input,
.field textarea {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.button {
  background: #4f46e5;
  color: white;
  padding: 0.5rem 1.5rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

Then import the styles in your component:

import React, { useState } from 'react';
import * as styles from './ContactForm.module.css';

function ContactForm() {
  const [status, setStatus] = useState('idle');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    const form = e.target;
    const data = new FormData(form);

    try {
      const res = await fetch(form.action, { method: 'POST', body: data });
      if (res.ok) { setStatus('success'); form.reset(); }
      else { setStatus('error'); }
    } catch { setStatus('error'); }
  }

  return (
    <form action="https://app.formwit.com/api/s/YOUR_FORM_ID" method="POST"
      onSubmit={handleSubmit} className={styles.form}>
      <div className={styles.field}>
        <label htmlFor="name">Name</label>
        <input type="text" id="name" name="name" required />
      </div>
      <div className={styles.field}>
        <label htmlFor="email">Email</label>
        <input type="email" id="email" name="email" required />
      </div>
      <div className={styles.field}>
        <label htmlFor="message">Message</label>
        <textarea id="message" name="message" rows="5" required />
      </div>
      <input type="text" name="_gotcha" style={{ display: 'none' }} tabIndex={-1} />
      <button type="submit" disabled={status === 'sending'} className={styles.button}>
        {status === 'sending' ? 'Sending...' : 'Send Message'}
      </button>
      {status === 'success' && <p>Message sent!</p>}
      {status === 'error' && <p>Something went wrong. Try again.</p>}
    </form>
  );
}

export default ContactForm;

Why not use a Gatsby serverless function?

Gatsby supports serverless functions (in src/api/) if you deploy to a platform like Gatsby Cloud or Netlify. You could write a function that sends email via SendGrid or Resend. But that means:

  • Managing API keys and environment variables for an email provider
  • Writing email sending logic and error handling yourself
  • No built-in submission dashboard or storage
  • No built-in spam protection
  • Your form only works on hosting platforms that support Gatsby Functions
  • More code to write and maintain

A form backend handles all of this out of the box. Your Gatsby site stays fully static and can be deployed anywhere: GitHub Pages, Netlify, Vercel, Cloudflare Pages, S3, or any other static host.

Using with Gatsby data sources

One of Gatsby's strengths is pulling data from CMSs like Contentful, Sanity, or WordPress via GraphQL. Your contact form works independently of all that. The form component doesn't interact with Gatsby's GraphQL data layer. It's a runtime-only component. This means you can add a contact form to any page, whether that page's content comes from Markdown files, a headless CMS, or is hardcoded in JSX.

Summary

Gatsby generates static sites, which means there's no server to handle form submissions. With FormWit, you create a form endpoint, build a standard React component, and get email notifications, submission storage, and spam protection without any server-side code or Gatsby plugins. The same component works in file-based pages, programmatic pages, and alongside your existing Gatsby plugins.

FormWit's free plan includes unlimited forms, 100 submissions per month, email notifications, and built-in spam protection. Create your free account and add a contact form to your Gatsby site in minutes.

Related guides: React contact form · Next.js contact form · HTML contact form · Spam protection · Contact form templates

Frequently asked questions

Does FormWit work with Gatsby?

Yes. FormWit works with any Gatsby project. The form submission happens in the browser via fetch, so it is completely independent of Gatsby's build process. No Gatsby plugins or gatsby-config.js changes needed.

Can I use React hooks with FormWit?

Yes. The example in this guide uses useState to track submission status (idle, sending, success, error). You can use any React hooks you want -- useRef for form references, useEffect for side effects, or custom hooks for form validation. FormWit is just a POST endpoint, so it works with any React pattern.

Does the form work after gatsby build?

Yes. The form works identically in development (gatsby develop) and production (gatsby build && gatsby serve). Form submissions happen at runtime in the browser, not during Gatsby's static build step. Deploy to any host and the form works immediately.

Want to skip the setup?

FormWit gives you a form endpoint in 60 seconds. Free plan, no credit card.

Create Free Form

Need a form fast?

Build one visually with our free HTML form generator — no coding required.

Try the Form Generator →

Add FormWit to your Gatsby site

Add a contact form to your site in 30 seconds. No backend code required.

Try FormWit Free