← All Guides
Next.js Forms
Add codaForms to your Next.js app. Works with App Router, Pages Router, and Server Actions.
5 minutes
Intermediate
Prerequisites
- A codaForms account (sign up free)
- A Next.js 13+ application
App Router (Recommended)
For Next.js 13+ with the app directory.
Client Component
Create a client component for the form:
app/components/ContactForm.tsx
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const FORM_ID = 'cf_xxxxx';
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('submitting');
const formData = new FormData(e.currentTarget);
const data = {
_formId: FORM_ID,
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
try {
const res = await fetch('https://codasite.ai/forms/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await res.json();
setStatus(result.success ? 'success' : 'error');
} catch {
setStatus('error');
}
}
if (status === 'success') {
return (
<div className="p-6 bg-green-500/10 rounded-lg text-center">
<p className="text-green-400">Thanks! We'll be in touch soon.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="name"
placeholder="Name"
required
className="w-full p-3 rounded-lg bg-gray-800 border border-gray-700"
/>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full p-3 rounded-lg bg-gray-800 border border-gray-700"
/>
<textarea
name="message"
placeholder="Message"
rows={4}
required
className="w-full p-3 rounded-lg bg-gray-800 border border-gray-700"
/>
<button
type="submit"
disabled={status === 'submitting'}
className="w-full p-3 bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{status === 'submitting' ? 'Sending...' : 'Send Message'}
</button>
{status === 'error' && (
<p className="text-red-400 text-sm">Something went wrong. Please try again.</p>
)}
</form>
);
} With Server Actions (Next.js 14+)
Use Server Actions for a more secure approach:
app/actions/submit-form.ts
'use server';
export async function submitForm(formData: FormData) {
const FORM_ID = 'cf_xxxxx';
const data = {
_formId: FORM_ID,
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
const res = await fetch('https://codasite.ai/forms/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await res.json();
return result;
} app/contact/page.tsx
'use client';
import { useFormStatus } from 'react-dom';
import { submitForm } from '../actions/submit-form';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Sending...' : 'Send Message'}
</button>
);
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<SubmitButton />
</form>
);
} Pages Router (Legacy)
For Next.js 12 or apps using the pages directory.
pages/contact.tsx
import { useState, FormEvent } from 'react';
export default function Contact() {
const [status, setStatus] = useState('idle');
const FORM_ID = 'cf_xxxxx';
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('submitting');
const form = e.currentTarget;
const formData = new FormData(form);
const res = await fetch('https://codasite.ai/forms/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
_formId: FORM_ID,
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}),
});
const result = await res.json();
setStatus(result.success ? 'success' : 'error');
}
if (status === 'success') {
return <p>Thanks! We'll be in touch.</p>;
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button disabled={status === 'submitting'}>
{status === 'submitting' ? 'Sending...' : 'Send'}
</button>
</form>
);
}