Using StaticForms with Gatsby: Complete Integration Guide
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
.envfiles 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! 🚀