Intermediate6 minUpdated Mar 27, 2026

How to Add a Contact Form to Remix

Add a contact form to your Remix app without writing a custom action. FormsList handles submissions so you can skip Remix action loaders entirely.

TL;DR

To add a contact form to Remix without a custom action, submit form data via fetch to `https://formslist.com/f/your-form-id` from a client-side handler. FormsList processes submissions so you don't need a Remix action loader.

By Vaibhav Jain·Published March 27, 2026

Prerequisites

  • A Remix project
  • A FormsList account (free)
  • Basic knowledge of React and Remix routing
1

Create a FormsList form endpoint

Sign up for a free FormsList account and create a new form. Copy the endpoint URL from your dashboard.

// Your endpoint will look like this:
// https://formslist.com/f/YOUR_FORM_HASH
2

Create the contact route component

Create a new route file in your Remix app. Instead of using a Remix action, we handle submission client-side with fetch. This keeps the form simple and avoids the need for a server-side action function.

// app/routes/contact.tsx
import { useState } from "react";
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = () => {
  return [
    { title: "Contact Us" },
    { name: "description", content: "Get in touch with us." },
  ];
};

export default function Contact() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");

    const form = e.currentTarget;
    const data = new FormData(form);

    try {
      const res = await fetch("https://formslist.com/f/YOUR_FORM_HASH", {
        method: "POST",
        body: data,
      });

      if (res.ok) {
        setStatus("sent");
        form.reset();
      } else {
        setStatus("error");
      }
    } catch {
      setStatus("error");
    }
  }

  return (
    <main className="mx-auto max-w-2xl py-12 px-4">
      <h1 className="text-3xl font-bold mb-6">Contact Us</h1>
      <form onSubmit={handleSubmit} className="space-y-4 max-w-md">
        <div>
          <label htmlFor="name" className="block text-sm font-medium">Name</label>
          <input
            type="text"
            id="name"
            name="name"
            required
            className="mt-1 block w-full rounded border p-2"
          />
        </div>
        <div>
          <label htmlFor="email" className="block text-sm font-medium">Email</label>
          <input
            type="email"
            id="email"
            name="email"
            required
            className="mt-1 block w-full rounded border p-2"
          />
        </div>
        <div>
          <label htmlFor="message" className="block text-sm font-medium">Message</label>
          <textarea
            id="message"
            name="message"
            rows={4}
            required
            className="mt-1 block w-full rounded border p-2"
          />
        </div>
        <button
          type="submit"
          disabled={status === "sending"}
          className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
        >
          {status === "sending" ? "Sending..." : "Send Message"}
        </button>
        {status === "sent" && <p className="text-green-600">Message sent successfully!</p>}
        {status === "error" && <p className="text-red-600">Something went wrong. Please try again.</p>}
      </form>
    </main>
  );
}
3

Add the route to your navigation

Link to the contact page from your site navigation or any other page using Remix's Link component.

import { Link } from "@remix-run/react";

<Link to="/contact">Contact Us</Link>
4

Test and configure notifications

Run `npm run dev`, navigate to /contact, and submit a test message. Verify it appears in your FormsList dashboard. Enable email notifications, auto-responders, or integrations from the dashboard.

Frequently Asked Questions

Why not use a Remix action for the form?

You can, but it adds complexity: you would need a server-side action function, validation, and email sending logic. With FormsList, a simple client-side fetch handles everything without any server code.

Can I still use progressive enhancement?

Yes. You can set the form's action attribute to your FormsList endpoint and method to POST as a fallback. The client-side fetch handler provides a better UX when JavaScript is available, and the native form submission works as a fallback.

Does this work with Remix deployed on Cloudflare Workers?

Yes. Since the form submission goes directly to FormsList from the client, it works regardless of where your Remix app is deployed: Vercel, Cloudflare Workers, Fly.io, or any other platform.

Ready to collect form submissions?

200 free submissions a month and a live endpoint in 30 seconds. No server, no credit card, just point your form at it.

Sign up with emailView pricing