Static Forms - Secure form backend and form endpoint for contact formsStatic Forms - Secure form backend and form endpoint for contact forms
  • Home
  • Features
  • Docs
  • Blog
  • Pricing
Register
  • Home
  • Features
  • Docs
  • Blog
  • Pricing
Back to all posts

Using StaticForms with Next.js: A Complete Guide

April 19, 2025
7 min read
Static Forms Team
next.jsformsstatic sitesserverless
Share:

Are you building a Next.js application and need a simple way to handle form submissions without setting up a backend server? Static Forms is the perfect solution for collecting form data from your static sites and receiving submissions directly in your inbox.

In this guide, we'll explore how to integrate the StaticForms service with your Next.js application, including both basic implementation and enhanced security with reCAPTCHA. If you're using Nuxt.js instead, check out our Nuxt.js integration guide.

What is StaticForms?

StaticForms is a form-to-email service designed specifically for static websites. It allows you to receive form submissions via email without writing any server-side code. This is particularly useful for:

  • Contact forms
  • Feedback forms
  • Newsletter signups
  • Simple data collection

The service is free to use (with some limitations) and provides a straightforward API for integrating forms into your static website.

Getting Started with StaticForms

Before implementing forms in your Next.js app, you'll need to:

  1. Visit staticforms.dev
  2. Register for a free account
  3. Obtain your unique API key

This API key will be used to connect your forms to your StaticForms account, ensuring submissions are forwarded to your email address.

Basic Integration in Next.js

Let's start by implementing a simple contact form without reCAPTCHA. This approach is quick to set up but offers less protection against spam.

Create a Client Component for Your Form

TSX
// app/components/contact-form.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    subject: "Contact Form Submission", // Default subject
    honeypot: "", // Anti-spam field
    replyTo: "@", // This will set replyTo to the email provided
    apiKey: "YOUR_API_KEY" // Replace with your actual API key
  });

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

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

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const res = await fetch("https://api.staticforms.dev/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 fields
        setFormData({
          ...formData,
          name: "",
          email: "",
          message: ""
        });
      } else {
        setResponse({
          type: "error",
          message: json.message || "An error occurred. Please try again."
        });
      }
    } catch (error) {
      console.error("Error submitting form:", error);
      setResponse({
        type: "error",
        message: "An error occurred while submitting the form. Please try again."
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="w-full max-w-md mx-auto">
      {response.type === "success" ? (
        <div className="p-4 mb-4 text-sm rounded-md bg-green-50 text-green-700">
          {response.message}
        </div>
      ) : response.type === "error" ? (
        <div className="p-4 mb-4 text-sm rounded-md bg-red-50 text-red-700">
          {response.message}
        </div>
      ) : null}

      <form onSubmit={handleSubmit} className="space-y-4">
        {/* Honeypot field to prevent spam */}
        <input
          type="text"
          name="honeypot"
          style={{ display: "none" }}
          onChange={handleChange}
        />

        <div>
          <label htmlFor="name" className="block text-sm font-medium mb-1">
            Name
          </label>
          <Input
            id="name"
            name="name"
            type="text"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </div>

        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            Email
          </label>
          <Input
            id="email"
            name="email"
            type="email"
            value={formData.email}
            onChange={handleChange}
            required
          />
        </div>

        <div>
          <label htmlFor="message" className="block text-sm font-medium mb-1">
            Message
          </label>
          <Textarea
            id="message"
            name="message"
            rows={4}
            value={formData.message}
            onChange={handleChange}
            required
          />
        </div>

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

Use the Form Component in Your Page

TSX
// app/contact/page.tsx
import ContactForm from "@/app/components/contact-form";

export const metadata = {
  title: "Contact Us",
  description: "Get in touch with our team"
};

export default function ContactPage() {
  return (
    <div className="container mx-auto py-12">
      <h1 className="text-3xl font-bold mb-8 text-center">Contact Us</h1>
      <ContactForm />
    </div>
  );
}

Enhanced Integration with reCAPTCHA

For better protection against spam and bot submissions, we can add Google reCAPTCHA to our form. This requires a few additional steps:

  1. Register for reCAPTCHA at Google reCAPTCHA and get your site key
  2. Add the reCAPTCHA component to your form

Contact Form with reCAPTCHA

TSX
// app/components/contact-form-with-recaptcha.tsx
"use client";

import { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import ReCAPTCHA from "react-google-recaptcha";

export default function ContactFormWithRecaptcha() {
  const recaptchaRef = useRef<ReCAPTCHA>(null);
  
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    subject: "Contact Form Submission",
    honeypot: "",
    replyTo: "@",
    apiKey: "YOUR_API_KEY", // Replace with your actual API key
    "g-recaptcha-response": "" // Will be filled programmatically
  });

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

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

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

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

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      // Execute reCAPTCHA and wait for the token
      let token = formData["g-recaptcha-response"];
      if (recaptchaRef.current && !token) {
        token = await recaptchaRef.current.executeAsync();
      }
      
      if (!token) {
        setResponse({
          type: "error",
          message: "Please complete the reCAPTCHA verification."
        });
        setIsSubmitting(false);
        return;
      }

      const submitData = {
        ...formData,
        "g-recaptcha-response": token
      };

      const res = await fetch("https://api.staticforms.dev/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 fields
        setFormData({
          ...formData,
          name: "",
          email: "",
          message: "",
          "g-recaptcha-response": ""
        });
        // Reset reCAPTCHA
        if (recaptchaRef.current) {
          recaptchaRef.current.reset();
        }
      } else {
        setResponse({
          type: "error",
          message: json.message || "An error occurred. Please try again."
        });
      }
    } catch (error) {
      console.error("Error submitting form:", error);
      setResponse({
        type: "error",
        message: "An error occurred while submitting the form. Please try again."
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="w-full max-w-md mx-auto">
      {response.type === "success" ? (
        <div className="p-4 mb-4 text-sm rounded-md bg-green-50 text-green-700">
          {response.message}
        </div>
      ) : response.type === "error" ? (
        <div className="p-4 mb-4 text-sm rounded-md bg-red-50 text-red-700">
          {response.message}
        </div>
      ) : null}

      <form onSubmit={handleSubmit} className="space-y-4">
        {/* Honeypot field to prevent spam */}
        <input
          type="text"
          name="honeypot"
          style={{ display: "none" }}
          onChange={handleChange}
        />

        {/* Invisible reCAPTCHA */}
        <ReCAPTCHA
          ref={recaptchaRef}
          size="invisible"
          sitekey="YOUR_RECAPTCHA_SITE_KEY" // Replace with your reCAPTCHA site key
          onChange={handleRecaptchaChange}
        />

        <div>
          <label htmlFor="name" className="block text-sm font-medium mb-1">
            Name
          </label>
          <Input
            id="name"
            name="name"
            type="text"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </div>

        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            Email
          </label>
          <Input
            id="email"
            name="email"
            type="email"
            value={formData.email}
            onChange={handleChange}
            required
          />
        </div>

        <div>
          <label htmlFor="message" className="block text-sm font-medium mb-1">
            Message
          </label>
          <Textarea
            id="message"
            name="message"
            rows={4}
            value={formData.message}
            onChange={handleChange}
            required
          />
        </div>

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

Installing Required Dependencies

Don't forget to install the necessary packages:

Bash
npm install react-google-recaptcha
# or
bun add react-google-recaptcha

Using Environment Variables for API Keys

For better security, it's recommended to use environment variables for your API keys instead of hardcoding them in your components:

  1. Create a .env.local file in your project root:
Plain Text
NEXT_PUBLIC_STATICFORMS_API_KEY=your_staticforms_api_key
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_recaptcha_site_key
  1. Update your components to use these environment variables:
TSX
// In your form component
apiKey: process.env.NEXT_PUBLIC_STATICFORMS_API_KEY
TSX
// In your reCAPTCHA component
sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}

Advanced Configuration Options

StaticForms offers several additional configuration options for enhancing your forms:

Custom Success/Error Redirects

You can specify custom URLs to redirect users after form submission:

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

Custom Fields

You can add custom fields to your form by prefixing the field name with $:

TSX
<div>
  <label htmlFor="company" className="block text-sm font-medium mb-1">
    Company
  </label>
  <Input
    id="company"
    name="$company" // Note the $ prefix
    type="text"
    value={formData.$company}
    onChange={handleChange}
  />
</div>

Troubleshooting Common Issues

Form Submissions Not Received

If you're not receiving form submissions:

  1. Double-check your API key is correct
  2. Make sure your email address is verified with StaticForms
  3. Check your spam folder

reCAPTCHA Not Working

If reCAPTCHA isn't functioning properly:

  1. Verify your site key is correct
  2. Ensure your domain is listed in the reCAPTCHA admin console
  3. Check for any JavaScript errors in the console

Conclusion

Integrating StaticForms with Next.js provides a simple, effective solution for handling form submissions in static sites without setting up a backend server. Whether you use the basic implementation or enhance it with reCAPTCHA for extra security, StaticForms makes the process straightforward.

For more information about StaticForms, check out our getting started guide or explore other integration examples for different frameworks, such as our Nuxt.js integration tutorial.

Remember that the free tier has some limitations, but it's perfect for most personal projects and small business websites. For larger projects with higher submission volumes, consider upgrading to a paid plan.

Happy coding with Next.js and StaticForms!

Previous

Understanding reCAPTCHA Integration with Static Forms

Next

Introducing New Plans and Policies for Static Forms

Related Articles

Using StaticForms with Nuxt.js: A Complete Guide

Learn how to integrate StaticForms into your Nuxt.js applications with and without reCAPTCHA protection.

Apr 27, 2025·8 min read

Netlify Forms vs Static Forms: Complete Comparison (2026)

Compare Netlify Forms and Static Forms features, pricing, AI Reply, and ease of use. Find the best form backend solution for your static website in 2026.

Jan 13, 2026·10 min read

Using StaticForms with Gatsby: Complete Integration Guide

Learn how to integrate StaticForms into your Gatsby sites with step-by-step examples, including reCAPTCHA and Altcha CAPTCHA protection.

Oct 25, 2025·8 min read
Static Forms - Secure form backend and form endpoint for contact formsStatic Forms - Secure form backend and form endpoint for contact forms

The fastest way to add working contact forms to any website. No backend required.

Product

  • Features
  • Pricing
  • Documentation
  • Changelog

Resources

  • Blog
  • Examples
  • Templates
  • Tools
  • Integrations
  • reCAPTCHA Guide
  • FAQ

Alternatives

  • All Alternatives
  • Formspree
  • Netlify Forms
  • Typeform
  • Formspark

Company

  • Contact
  • About

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DPA

© 2026 Static Forms. All rights reserved.

Committed to sustainability