Next.js
React Forms with Next.js
Next.js App Router client components can submit to Formward with a simple fetch call, giving you full control over loading states and success feedback without any server-side code.
01Create your form in Formward
Sign up at formward.eu, create a new form, and copy the endpoint URL (https://formward.eu/f/your-form-id).
02Create a ContactForm client component
Add the file app/contact/ContactForm.tsx (or .jsx) with the 'use client' directive. The component uses controlled inputs and fetch to POST to Formward with Accept: application/json, avoiding a full-page redirect.
// app/contact/ContactForm.tsx 'use client'; import { useState } from 'react'; interface Fields { name: string; email: string; message: string; } export default function ContactForm() { const [fields, setFields] = useState<Fields>({ name: '', email: '', message: '' }); const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle'); function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) { setFields({ ...fields, [e.target.name]: e.target.value }); } async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); setStatus('submitting'); try { const res = await fetch('https://formward.eu/f/your-form-id', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: new URLSearchParams({ ...fields, _gotcha: '' }).toString(), }); if (!res.ok) throw new Error('Network error'); setStatus('success'); setFields({ name: '', email: '', message: '' }); } catch { setStatus('error'); } } if (status === 'success') { return <p className="text-green-700">Thank you, your message has been sent!</p>; } return ( <form onSubmit={handleSubmit} className="flex flex-col gap-4 max-w-md"> <label className="flex flex-col gap-1 text-sm font-medium"> Name <input type="text" name="name" value={fields.name} onChange={handleChange} required className="rounded border border-gray-300 px-3 py-2 text-sm" /> </label> <label className="flex flex-col gap-1 text-sm font-medium"> Email <input type="email" name="email" value={fields.email} onChange={handleChange} required className="rounded border border-gray-300 px-3 py-2 text-sm" /> </label> <label className="flex flex-col gap-1 text-sm font-medium"> Message <textarea name="message" value={fields.message} onChange={handleChange} rows={5} required className="rounded border border-gray-300 px-3 py-2 text-sm resize-y" /> </label> {/* honeypot: keep hidden */} <input type="text" name="_gotcha" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" /> <button type="submit" disabled={status === 'submitting'} className="self-start rounded bg-black px-5 py-2 text-sm text-white disabled:opacity-50"> {status === 'submitting' ? 'Sending…' : 'Send'} </button> {status === 'error' && ( <p className="text-red-600 text-sm">Something went wrong. Please try again.</p> )} </form> ); }03Render on a page
Create app/contact/page.tsx as a server component that imports and renders ContactForm. Because the form component carries 'use client', Next.js automatically bundles it for the browser while keeping the page shell server-rendered.
// app/contact/page.tsx import ContactForm from './ContactForm'; export const metadata = { title: 'Contact' }; export default function ContactPage() { return ( <main className="mx-auto max-w-2xl px-4 py-16"> <h1 className="text-3xl font-bold mb-8">Get in touch</h1> <ContactForm /> </main> ); }04Test and view submissions
Run next dev, open http://localhost:3000/contact, and submit the form. The component shows a success message without navigating away. Open the Formward dashboard to confirm the submission arrived.