Back to all posts

Using StaticForms with Gatsby: Complete Integration Guide

8 min read
Static Forms Team

Building a static site with Gatsby doesn't mean you have to sacrifice form functionality. StaticForms makes it easy to add contact forms and other data collection features to your Gatsby site without writing backend code or managing servers.

In this guide, we'll walk through integrating StaticForms with Gatsby, including basic setup and spam protection with reCAPTCHA or privacy-first Altcha CAPTCHA.

Why StaticForms for Gatsby?

Gatsby generates static HTML at build time with no server to process forms. StaticForms bridges this gap with:

  • No Backend Required: Handle forms without servers or serverless functions
  • Instant Setup: Add your API key and start receiving submissions
  • Email Notifications: Get submissions delivered to your inbox
  • Spam Protection: Honeypot, reCAPTCHA, and Altcha CAPTCHA options
  • File Uploads: Accept attachments up to specified limits
  • Webhook Integration: Connect to external services (Pro feature)

Prerequisites

  • A Gatsby site (version 4.x or 5.x)
  • A StaticForms account (sign up here)
  • Your API key from your dashboard

Basic Contact Form

Create a contact form component in your Gatsby project:

// src/components/contact-form.js
import React, { useState } from "react";
import "../styles/contact-form.css"; // Optional: add your styles

const ContactForm = () => {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    subject: "New Contact Form Submission",
    honeypot: "", // Anti-spam honeypot field
    replyTo: "@", // Reply to the sender's email
    apiKey: "YOUR_API_KEY" // Replace with your StaticForms API key
  });

  const [response, setResponse] = useState({
    type: "",
    message: ""
  });

  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const res = await fetch("https://api.staticforms.xyz/submit", {
        method: "POST",
        body: JSON.stringify(formData),
        headers: { "Content-Type": "application/json" }
      });

      const json = await res.json();

      if (json.success) {
        setResponse({
          type: "success",
          message: "Thank you for your message! We'll get back to you soon."
        });
        // Reset form
        setFormData({
          ...formData,
          name: "",
          email: "",
          message: ""
        });
      } else {
        setResponse({
          type: "error",
          message: json.message || "Something went wrong. Please try again."
        });
      }
    } catch (error) {
      console.error("Form submission error:", error);
      setResponse({
        type: "error",
        message: "Failed to submit form. Please check your connection and try again."
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="contact-form-wrapper">
      {response.type === "success" && (
        <div className="alert alert-success">
          {response.message}
        </div>
      )}
      
      {response.type === "error" && (
        <div className="alert alert-error">
          {response.message}
        </div>
      )}

      <form onSubmit={handleSubmit} className="contact-form">
        {/* Honeypot field - hidden from users, catches bots */}
        <input
          type="text"
          name="honeypot"
          value={formData.honeypot}
          onChange={handleChange}
          style={{ display: "none" }}
          tabIndex="-1"
          autoComplete="off"
        />

        <div className="form-group">
          <label htmlFor="name">
            Name <span className="required">*</span>
          </label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            placeholder="Your name"
          />
        </div>

        <div className="form-group">
          <label htmlFor="email">
            Email <span className="required">*</span>
          </label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            placeholder="your.email@example.com"
          />
        </div>

        <div className="form-group">
          <label htmlFor="message">
            Message <span className="required">*</span>
          </label>
          <textarea
            id="message"
            name="message"
            value={formData.message}
            onChange={handleChange}
            required
            rows="5"
            placeholder="Your message..."
          />
        </div>

        <button 
          type="submit" 
          disabled={isSubmitting}
          className="submit-button"
        >
          {isSubmitting ? "Sending..." : "Send Message"}
        </button>
      </form>
    </div>
  );
};

export default ContactForm;

Add the form to any page:

// src/pages/contact.js
import React from "react";
import Layout from "../components/layout";
import ContactForm from "../components/contact-form";

const ContactPage = () => (
  <Layout>
    <h1>Contact Us</h1>
    <ContactForm />
  </Layout>
);

export default ContactPage;

That's it! You now have a fully functional contact form. 🎉

Adding reCAPTCHA Protection

Install the package and get your keys from Google reCAPTCHA Admin:

npm install react-google-recaptcha

Add reCAPTCHA to your form:

// src/components/contact-form-recaptcha.js
import React, { useState, useRef } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import "../styles/contact-form.css";

const ContactFormWithRecaptcha = () => {
  const recaptchaRef = useRef();
  
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    subject: "New Contact Form Submission",
    honeypot: "",
    replyTo: "@",
    apiKey: "YOUR_STATICFORMS_API_KEY", // Replace with your API key
    "g-recaptcha-response": "" // reCAPTCHA token
  });

  const [response, setResponse] = useState({
    type: "",
    message: ""
  });

  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };

  const handleRecaptchaChange = (token) => {
    setFormData({
      ...formData,
      "g-recaptcha-response": token || ""
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Verify reCAPTCHA is completed
    if (!formData["g-recaptcha-response"]) {
      setResponse({
        type: "error",
        message: "Please complete the reCAPTCHA challenge."
      });
      return;
    }
    
    setIsSubmitting(true);
    
    try {
      const res = await fetch("https://api.staticforms.xyz/submit", {
        method: "POST",
        body: JSON.stringify(formData),
        headers: { "Content-Type": "application/json" }
      });

      const json = await res.json();

      if (json.success) {
        setResponse({
          type: "success",
          message: "Thank you for your message! We'll get back to you soon."
        });
        // Reset form
        setFormData({
          ...formData,
          name: "",
          email: "",
          message: "",
          "g-recaptcha-response": ""
        });
        // Reset reCAPTCHA
        if (recaptchaRef.current) {
          recaptchaRef.current.reset();
        }
      } else {
        setResponse({
          type: "error",
          message: json.message || "Something went wrong. Please try again."
        });
      }
    } catch (error) {
      console.error("Form submission error:", error);
      setResponse({
        type: "error",
        message: "Failed to submit form. Please check your connection and try again."
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="contact-form-wrapper">
      {response.type === "success" && (
        <div className="alert alert-success">
          {response.message}
        </div>
      )}
      
      {response.type === "error" && (
        <div className="alert alert-error">
          {response.message}
        </div>
      )}

      <form onSubmit={handleSubmit} className="contact-form">
        {/* Honeypot field */}
        <input
          type="text"
          name="honeypot"
          value={formData.honeypot}
          onChange={handleChange}
          style={{ display: "none" }}
          tabIndex="-1"
          autoComplete="off"
        />

        <div className="form-group">
          <label htmlFor="name">
            Name <span className="required">*</span>
          </label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            placeholder="Your name"
          />
        </div>

        <div className="form-group">
          <label htmlFor="email">
            Email <span className="required">*</span>
          </label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            placeholder="your.email@example.com"
          />
        </div>

        <div className="form-group">
          <label htmlFor="message">
            Message <span className="required">*</span>
          </label>
          <textarea
            id="message"
            name="message"
            value={formData.message}
            onChange={handleChange}
            required
            rows="5"
            placeholder="Your message..."
          />
        </div>

        <div className="form-group">
          <ReCAPTCHA
            ref={recaptchaRef}
            sitekey="YOUR_RECAPTCHA_SITE_KEY" // Replace with your reCAPTCHA site key
            onChange={handleRecaptchaChange}
          />
        </div>

        <button 
          type="submit" 
          disabled={isSubmitting}
          className="submit-button"
        >
          {isSubmitting ? "Sending..." : "Send Message"}
        </button>
      </form>
    </div>
  );
};

export default ContactFormWithRecaptcha;

Learn more in our reCAPTCHA guide.

Privacy-First: Altcha CAPTCHA

For GDPR-compliant spam protection without tracking, use Altcha CAPTCHA (Pro feature):

// src/components/contact-form-altcha.js
import React, { useState, useRef, useEffect } from "react";
import "../styles/contact-form.css";

const ContactFormWithAltcha = () => {
  const altchaWidgetRef = useRef(null);
  
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    subject: "New Contact Form Submission",
    honeypot: "",
    replyTo: "@",
    apiKey: "YOUR_STATICFORMS_API_KEY" // Replace with your API key
  });

  const [response, setResponse] = useState({
    type: "",
    message: ""
  });

  const [isSubmitting, setIsSubmitting] = useState(false);

  // Load Altcha script
  useEffect(() => {
    const script = document.createElement("script");
    script.src = "https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js";
    script.type = "module";
    document.head.appendChild(script);

    return () => {
      document.head.removeChild(script);
    };
  }, []);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Get Altcha payload from the widget
    const altchaPayload = altchaWidgetRef.current?.getAttribute("data-payload");
    
    if (!altchaPayload) {
      setResponse({
        type: "error",
        message: "Please complete the CAPTCHA challenge."
      });
      return;
    }
    
    setIsSubmitting(true);
    
    try {
      const submitData = {
        ...formData,
        altchaPayload // Include Altcha payload
      };

      const res = await fetch("https://api.staticforms.xyz/submit", {
        method: "POST",
        body: JSON.stringify(submitData),
        headers: { "Content-Type": "application/json" }
      });

      const json = await res.json();

      if (json.success) {
        setResponse({
          type: "success",
          message: "Thank you for your message! We'll get back to you soon."
        });
        // Reset form
        setFormData({
          ...formData,
          name: "",
          email: "",
          message: ""
        });
        // Reset Altcha widget
        if (altchaWidgetRef.current) {
          altchaWidgetRef.current.reset();
        }
      } else {
        setResponse({
          type: "error",
          message: json.message || "Something went wrong. Please try again."
        });
      }
    } catch (error) {
      console.error("Form submission error:", error);
      setResponse({
        type: "error",
        message: "Failed to submit form. Please check your connection and try again."
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="contact-form-wrapper">
      {response.type === "success" && (
        <div className="alert alert-success">
          {response.message}
        </div>
      )}
      
      {response.type === "error" && (
        <div className="alert alert-error">
          {response.message}
        </div>
      )}

      <form onSubmit={handleSubmit} className="contact-form">
        {/* Honeypot field */}
        <input
          type="text"
          name="honeypot"
          value={formData.honeypot}
          onChange={handleChange}
          style={{ display: "none" }}
          tabIndex="-1"
          autoComplete="off"
        />

        <div className="form-group">
          <label htmlFor="name">
            Name <span className="required">*</span>
          </label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            placeholder="Your name"
          />
        </div>

        <div className="form-group">
          <label htmlFor="email">
            Email <span className="required">*</span>
          </label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            placeholder="your.email@example.com"
          />
        </div>

        <div className="form-group">
          <label htmlFor="message">
            Message <span className="required">*</span>
          </label>
          <textarea
            id="message"
            name="message"
            value={formData.message}
            onChange={handleChange}
            required
            rows="5"
            placeholder="Your message..."
          />
        </div>

        <div className="form-group">
          <altcha-widget
            ref={altchaWidgetRef}
            challengeurl="https://www.staticforms.xyz/api/altcha/challenge"
            hidefooter
          />
        </div>

        <button 
          type="submit" 
          disabled={isSubmitting}
          className="submit-button"
        >
          {isSubmitting ? "Sending..." : "Send Message"}
        </button>
      </form>
    </div>
  );
};

export default ContactFormWithAltcha;

See our Altcha implementation guide for details.

Using Environment Variables

Protect your API keys with environment variables. Create .env.development:

GATSBY_STATICFORMS_API_KEY=your_api_key
GATSBY_RECAPTCHA_SITE_KEY=your_recaptcha_key

Use in your components:

apiKey: process.env.GATSBY_STATICFORMS_API_KEY

// For reCAPTCHA
<ReCAPTCHA sitekey={process.env.GATSBY_RECAPTCHA_SITE_KEY} />

Important: Gatsby requires the GATSBY_ prefix for browser-accessible variables.

Advanced Features

Custom Redirects

const [formData, setFormData] = useState({
  // ...other fields
  redirectTo: "https://yoursite.com/thank-you",
  apiKey: process.env.GATSBY_STATICFORMS_API_KEY
});

Custom Fields

Add extra data with $ prefix:

<input
  type="text"
  name="$company"
  placeholder="Company name"
/>

File Uploads

See our file upload tutorial for implementation details.

Troubleshooting

Not receiving emails?

  • Verify your API key in your dashboard
  • Check spam folder
  • Confirm email verification with StaticForms

Build errors?

  • Restart Gatsby dev server after adding environment variables
  • Check GATSBY_ prefix on all variables
  • Ensure dependencies are installed

Environment variables not working?

  • Variables must be in project root
  • Restart server after changes
  • Verify .env files aren't in .gitignore

Conclusion

StaticForms provides a maintenance-free form solution for Gatsby sites. Choose from honeypot, reCAPTCHA, or privacy-focused Altcha CAPTCHA for spam protection. The free tier works great for personal projects, while Pro plans offer advanced features for larger sites.

Ready to add forms to your Gatsby site? Sign up for StaticForms and get started in minutes!

For other frameworks, check out our guides for Next.js and Nuxt.js.

Happy coding! 🚀