Integrating Contact Form 7 with Next.js

Headless WordPressNext.js
Integrating Contact Form 7 with Next.js Cover

This guide demonstrates the implementation of a headless contact form using Next.js and WordPress Contact Form 7. You’ll learn how to create a form, handle submissions, and implement validation while maintaining a seamless user experience.

Alternative premium implementation: Integrating Gravity Forms with Next.js

Requirements

  • WordPress installation with Contact Form 7 plugin
  • Next.js 15 application with App Router

Implementation

WordPress Configuration

Contact Form 7 Setup

  1. Create a new form in Contact Form 7 with the following fields. These field names are crucial as they’ll be used in our Next.js implementation:
<label> First name [text* first-name] </label>
<label> Last name [text* last-name] </label>
<label> Email [email* email] </label>
<label> Message [textarea* message] </label>
[submit "Submit"]

New form with first name, last name, email and message as fields

  1. Configure the Mail tab settings to match your field names. This ensures emails are sent with the correct information:

Mail tab settings

  1. For storing form submissions, install Flamingo. This plugin provides a database to store form submissions in WordPress.

Note: Flamingo uses default input fields: your-subject, your-name, and your-email. To customize these, add the following to the Additional Settings tab. This ensures submissions are properly labeled in the Flamingo database:

flamingo_name: "[first-name] [last-name]"
flamingo_email: "[email]"
flamingo_subject: "Form submission"

Next.js Implementation

1. UI Components Setup (Optional)

We’ll use shadcn/ui for a polished, accessible form interface. Install it with:

Terminal window
npx shadcn@latest init

Add the required components that we’ll use in our form:

Terminal window
npx shadcn@latest add button input textarea label

2. Form Component

Create the base form structure. Note that the field names must match exactly with those in Contact Form 7:

src/components/contact-form.tsx
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Textarea } from "./ui/textarea"
export function ContactForm() {
return (
<form className="w-full max-w-3xl rounded-lg bg-white px-4 py-12">
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" name="first-name" />
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" name="last-name" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" />
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" name="message" />
</div>
<Button type="submit">Send</Button>
</div>
</form>
)
}

Add to homepage:

src/app/page.tsx
import { ContactForm } from "@/components/contact-form"
export default function HomePage() {
return (
<main className="flex min-h-screen items-center justify-center bg-slate-200">
<ContactForm />
</main>
)
}

Next.js form

3. Form Submission Handler

Before implementing form submission, you’ll need two pieces of information from your Contact Form 7 setup:

  1. Contact Form ID: Found in the URL as post=<FORM_ID>
  2. Unit Tag: 7-character alphanumeric string in the form’s shortcode

These values are required for the API endpoint and form submission:

Retrieve Contact Form 7 id and unit tag

Create a server action to handle form submissions:

src/server/actions.ts
"use server"
export async function sendFormData(formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39") // Replace with your Unit Tag
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://yt-cf7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
}
}
}

4. Form Validation

Contact Form 7 provides validation feedback through its API response. Here’s an example of the response structure when validation fails:

{
"contact_form_id": 8,
"status": "validation_failed",
"message": "One or more fields have an error. Please check and try again.",
"invalid_fields": [
{
"field": "last-name",
"message": "Please fill out this field.",
"idref": null,
"error_id": "21e9d39-ve-last-name"
}
]
}

To make the validation messages more accessible in our form, we’ll transform the response:

src/server/actions.ts
"use server"
export async function sendFormData(formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39") // Replace with your Unit Tag
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://yt-cf7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
if (data.invalid_fields.length) {
data.invalid_fields_obj = {}
data.invalid_fields.forEach(
(field: { field: string; message: string }) => {
data.invalid_fields_obj[field.field] = field.message
},
)
}
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
}
}
}

The form component is marked as a client component to handle client-side state and interactions. It uses the useActionState hook to manage form submission state and displays validation messages:

src/components/contact-form.tsx
"use client"
import { useActionState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Textarea } from "./ui/textarea"
import { sendFormData } from "@/server/actions"
export function ContactForm() {
const [state, action, isPending] = useActionState(sendFormData, {})
return (
<form
action={action}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" name="first-name" />
{state.invalid_fields_obj?.["first-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["first-name"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" name="last-name" />
{state.invalid_fields_obj?.["last-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["last-name"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" />
{state.invalid_fields_obj?.["email"] && (
<p className="text-destructive">
{state.invalid_fields_obj["email"]}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" name="message" />
{state.invalid_fields_obj?.["message"] && (
<p className="text-destructive">
{state.invalid_fields_obj["message"]}
</p>
)}
</div>
<Button type="submit">Send</Button>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
<p className={state.status === "mail_sent" ? "" : "text-destructive"}>
{state.message}
</p>
</div>
</form>
)
}

The function passed to useActionState receives an extra argument, the previous or initial state, as its first argument:

src/server/actions.ts
"use server"
export async function sendFormData(prevState: unknown, formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39") // Replace with your Unit Tag
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://yt-cf7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
if (data.invalid_fields.length) {
data.invalid_fields_obj = {}
data.invalid_fields.forEach(
(field: { field: string; message: string }) => {
data.invalid_fields_obj[field.field] = field.message
},
)
}
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
}
}
}

The validation messages are displayed with a destructive (error) styling and appear only when validation fails for that specific field. The form uses the isPending state from useActionState to disable the submit button and show a loading state during submission. Success and error messages are displayed at the bottom of the form based on the submission state.

Form with validation errors

5. Preserving Form Input Values After Validation Errors

When form validation fails, we want to preserve the user’s input to avoid data loss. The server action returns the form data in the response, which we use to populate the form fields:

src/server/actions.ts
"use server"
export async function sendFormData(prevState: unknown, formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39") // Replace with your Unit Tag
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://yt-cf7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
if (data.invalid_fields.length) {
data.payload = formData
data.invalid_fields_obj = {}
data.invalid_fields.forEach(
(field: { field: string; message: string }) => {
data.invalid_fields_obj[field.field] = field.message
},
)
}
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
payload: formData,
}
}
}

The form component uses this returned data to set the default values of input fields:

src/components/contact-form.tsx
"use client"
import { useActionState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Textarea } from "./ui/textarea"
import { sendFormData } from "@/server/actions"
export function ContactForm() {
const [state, action, isPending] = useActionState(sendFormData, {})
return (
<form
action={action}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input
id="first-name"
name="first-name"
defaultValue={state.payload?.get("first-name") || ""}
/>
{state.invalid_fields_obj?.["first-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["first-name"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input
id="last-name"
name="last-name"
defaultValue={state.payload?.get("last-name") || ""}
/>
{state.invalid_fields_obj?.["last-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["last-name"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
defaultValue={state.payload?.get("email") || ""}
/>
{state.invalid_fields_obj?.["email"] && (
<p className="text-destructive">
{state.invalid_fields_obj["email"]}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea
id="message"
name="message"
defaultValue={state.payload?.get("message") || ""}
/>
{state.invalid_fields_obj?.["message"] && (
<p className="text-destructive">
{state.invalid_fields_obj["message"]}
</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
<p className={state.status === "mail_sent" ? "" : "text-destructive"}>
{state.message}
</p>
</div>
</form>
)
}

This ensures that users don’t have to re-enter their data when validation errors occur.

Form with user inputs

Conclusion

This implementation provides a complete solution for integrating Contact Form 7 with Next.js, including:

  • Server-side form submission using Next.js Server Actions
  • Client-side validation with Contact Form 7 API
  • Form state preservation on validation errors
  • Error handling and user feedback
  • Responsive UI components using shadcn/ui