FormRill
  1. Docs
  2. 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 free

Related

Last updated