Next.js Forms
In Next.js projects using the App Router, you can easily build responsive contact forms as client components ("use client") and POST submissions directly to Lucid Forms. This avoids complex Next.js API route or Server Action setup.
App Router Client Component Example
Section titled “App Router Client Component Example”Create a file named components/ContactForm.tsx in your Next.js project. It uses standard React hooks and can be styled with Tailwind CSS or standard CSS modules.
"use client";
import React, { useState } from "react";
export default function ContactForm() { const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setStatus("loading"); setErrorMessage("");
const formData = new FormData(e.currentTarget); const data = Object.fromEntries(formData.entries());
// Bot trap check if (data._honeypot) { setStatus("success"); return; }
try { const response = await fetch("https://api.lucidforms.co/f/{your-form-id}", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ name: data.name, email: data.email, message: data.message, }), });
const responseData = await response.json();
if (response.ok) { setStatus("success"); (e.target as HTMLFormElement).reset(); } else { setStatus("error"); setErrorMessage(responseData.error || "Form submission failed. Please try again."); } } catch (err) { setStatus("error"); setErrorMessage("A network error occurred. Please try again."); } };
return ( <div className="mx-auto max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-zinc-950"> <h2 className="mb-4 text-xl font-bold text-gray-900 dark:text-white">Get in Touch</h2>
{status === "success" && ( <div className="mb-4 rounded bg-emerald-50 p-3 text-sm font-medium text-emerald-800 dark:bg-emerald-950/20 dark:text-emerald-400"> ✓ Your message has been sent successfully! </div> )}
{status === "error" && ( <div className="mb-4 rounded bg-rose-50 p-3 text-sm font-medium text-rose-800 dark:bg-rose-950/20 dark:text-rose-400"> ✗ {errorMessage} </div> )}
<form onSubmit={handleSubmit} className="space-y-4"> {/* Invisible honeypot trap */} <input type="text" name="_honeypot" className="hidden" tabIndex={-1} autoComplete="off" />
<div> <label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> Full Name </label> <input type="text" id="name" name="name" required className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-800 dark:bg-zinc-900 dark:text-white" /> </div>
<div> <label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> Email Address </label> <input type="email" id="email" name="email" required className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-800 dark:bg-zinc-900 dark:text-white" /> </div>
<div> <label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300"> Message </label> <textarea id="message" name="message" required rows={4} className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-800 dark:bg-zinc-900 dark:text-white" /> </div>
<button type="submit" disabled={status === "loading"} className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 dark:bg-blue-700 dark:hover:bg-blue-600" > {status === "loading" ? "Sending..." : "Submit"} </button> </form> </div> );}File Upload Handling in Next.js
Section titled “File Upload Handling in Next.js”If you are accepting user attachments (e.g., resumes, screenshots) and have a Pro Plan which supports file uploads up to 1GB, submit the data using the standard FormData object instead of converting to JSON.
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setStatus("loading");
const formData = new FormData(e.currentTarget);
try { const response = await fetch("https://api.lucidforms.co/f/{your-form-id}", { method: "POST", headers: { // Do NOT set Content-Type header when sending FormData! // The browser automatically sets it with the correct boundary parameters. "Accept": "application/json", }, body: formData, });
if (response.ok) { setStatus("success"); e.currentTarget.reset(); } else { setStatus("error"); } } catch (err) { setStatus("error"); }};Make sure you include the file input in your JSX markup:
<input type="file" name="attachment" />