- Docs
- Quick starts
Next.js contact form without an API route
A client component that posts straight to FormRill. No route handler, no server action and no email provider to configure, so it works the same on Vercel, any Node host, or a fully static output: 'export' build.
1. Add your form ID
NEXT_PUBLIC_FORMRILL_FORM_ID=f_…Copy the ID from the Form URL on your form’s Setup tab. It’s safe in the browser bundle: the URL can only submit, and reading submissions needs your dashboard login.
2. Create the component
'use client'
import { useState, type FormEvent } from 'react'
const FORM_URL = `https://api.formrill.com/${process.env.NEXT_PUBLIC_FORMRILL_FORM_ID}/submit`
type Status = 'idle' | 'sending' | 'sent' | 'error'
export default function ContactForm() {
const [status, setStatus] = useState<Status>('idle')
const [error, setError] = useState('')
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const form = event.currentTarget
setStatus('sending')
setError('')
try {
const res = await fetch(FORM_URL, {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' },
})
const result = await res.json()
if (!result.ok) throw new Error(result.error ?? 'Something went wrong')
form.reset()
setStatus('sent')
} catch (err) {
setError(err instanceof Error ? err.message : 'Network error')
setStatus('error')
}
}
if (status === 'sent') return <p role="status">Thanks! We’ll be in touch.</p>
return (
<form action={FORM_URL} method="POST" onSubmit={onSubmit}>
<input type="checkbox" name="botcheck" hidden tabIndex={-1} autoComplete="off" />
<label>Name <input name="name" required autoComplete="name" /></label>
<label>Email <input type="email" name="email" required autoComplete="email" /></label>
<label>Message <textarea name="message" required /></label>
{status === 'error' && <p role="alert">Couldn’t send: {error}</p>}
<button disabled={status === 'sending'}>
{status === 'sending' ? 'Sending…' : 'Send'}
</button>
</form>
)
}The action and method stay on the form so it still submits if JavaScript hasn’t loaded yet; FormRill then shows its own thank-you page, or your redirect.
3. Render it from a page
import ContactForm from './contact-form'
export const metadata = { title: 'Contact' }
export default function ContactPage() {
return (
<main>
<h1>Contact us</h1>
<ContactForm />
</main>
)
}The page stays a server component and can be statically rendered; only the form ships JavaScript.
What the responses look like
Success is { "ok": true, "id": "s_…" }. Errors are { "ok": false, "error": "…" } with a 4xx status, for example 403 when the domain whitelist doesn’t include your site (add localhost while developing, or leave the list empty). The full table is in the API reference.
Pages Router?
The same component works without changes in pages/; drop the 'use client' line if you like, it is ignored there.
Get the form URL for this guide
Free up to 500 submissions a month and unlimited forms. No server, no credit card.
Start freeRelated
- Submission API reference
Every field, status code and limit.
- Forms on v0, Lovable and Bolt sites
Fixing AI-generated forms that don’t send.
- formrill.js
Funnel analytics and validation.