integrations · framework

A contact form for React, without building a backend.

A React SPA has nowhere to send a contact form: there's no server behind a Vite build. You don't need to add one. POST the fields to your mailcontact endpoint straight from the browser (CORS is open, so it works from any origin) and messages land in your project's inbox, with a notification in Discord, Slack, email, or a webhook.

The direct API alternative below uses a form, a fetch call, and a useState flag for the success state. No Express server, no serverless function, no email provider SDK. Your build stays a pile of static files.

Prefer zero JavaScript? A plain <form action="…" method="POST"> posts to the same endpoint. See the FAQ for the trade-off.

recommended setup

Use the MailContact script with your own design.

Add this HTML to your site, or use the form builder. Replace the form link with yours. The hosted script injects the hidden check, handles sending and automatic verification, and preserves the draft on errors. You control the HTML and CSS. Free forms display a visible “Processed by mailcontact.app” link; Pro and Plus do not. Feedback follows the browser’s language; set data-mailcontact-language="fr" on the form to force French.

contact.html
<form action="https://mailcontact.app/api/forms/your-form-id" method="POST" data-mailcontact-form>
  <fieldset>
    <legend>Contact us</legend>
    <label>
      Name (optional)
      <input name="name" type="text" autocomplete="name" maxlength="200" />
    </label>
    <label>
      Email
      <input name="email" type="email" autocomplete="email" maxlength="320" required />
    </label>
    <label>
      Message
      <textarea name="message" rows="5" maxlength="5000" required></textarea>
    </label>
    <noscript>
    <div aria-hidden="true" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;">
      <label>Website
        <input name="company_website" type="text" tabindex="-1" autocomplete="off" />
      </label>
    </div>
    </noscript>
    <button type="submit">Send message</button>
  </fieldset>
  <p data-contact-status role="status" aria-live="polite" aria-atomic="true"></p>
</form>
<script defer src="https://mailcontact.app/forms/v1.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

For a component with its own submit handler, load the same script and use MailContact.prepare(form) and MailContact.send() instead of the automatic marker. See the custom interface reference for loading, feedback and Free attribution.

the snippet

Alternative: a direct integration for React.

This example posts directly to the API. Keep the required company_website field present and empty. Native posts open JSON on success or a verification page when needed. Custom scripts without verification support send suspicious messages to Spam for review; strong spam is also held there without notifications or quota usage. A success response confirms reception, including review, or a silent discard of a filled check. See spam protection for the full behavior.

src/components/ContactForm.tsx
import { useState } from "react";

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

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setStatus("sending");
    try {
      const res = await fetch("https://mailcontact.app/api/forms/your-form-id", {
        method: "POST",
        body: data,
      });
      setStatus(res.ok ? "sent" : "error");
    } catch {
      setStatus("error");
    }
  }

  if (status === "sent") {
    return <p>Thanks! Your message is on its way.</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" placeholder="Your name" />
      <input type="email" name="email" placeholder="[email protected]" required />
      <textarea name="message" placeholder="What's on your mind?" required />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" && <p>Something went wrong. Please try again.</p>}
      <input name="company_website" type="text" tabIndex={-1} autoComplete="off"
        style={{ position: "absolute", left: "-10000px" }} aria-hidden="true" />
    </form>
  );
}

how it works

Three steps, start to finish.

01

Create an inbox

One inbox per project. Name it after your app and you get a form endpoint and a project email address immediately.

02

Drop in the component

Paste ContactForm.tsx, swap in your form id, and render it anywhere: a route, a footer, a modal. The markup is yours to restyle.

03

Get notified

Add a Discord, Slack, email, or webhook channel. Delivered messages ping you where you already are.

why mailcontact

Built to pair well with React.

No server to bolt onto your SPA

A Vite or CRA build is static files. Because the endpoint accepts cross-origin requests, the browser talks to mailcontact directly. No proxy, no serverless function, nothing extra to deploy.

Success states that feel like React

fetch plus useState keeps visitors inside your app: disable the button while sending, swap in a thank-you message on success. No full-page navigation out of your client-side router.

Your JSX, your design system

It's a plain form component, not an embedded widget or iframe. Style it with Tailwind, CSS Modules, styled-components, whatever the rest of your app uses.

faq

React + mailcontact, answered

Do I really not need a backend or serverless function?

Correct. The endpoint accepts cross-origin POSTs (CORS is open), so the browser submits directly to mailcontact. Your React app can be deployed as pure static files with no functions at all.

How do I show a success message without leaving the page?

Submit with fetch as in the snippet and flip a useState flag when the response comes back ok. Native form posts currently return a JSON response instead of redirecting back to your site (a configurable redirect is not currently available), so fetch is the right fit for a SPA today.

Which fields does the endpoint accept, and what are the limits?

Send email and message (both required), optional name and subject, and the required empty company_website check, as FormData or JSON. Requests are limited to 20 per minute per IP with a 64KB max body, plenty for a contact form.

Can I use react-hook-form or my component library?

Yes. mailcontact only sees the POST, so build the form however you like (react-hook-form, controlled inputs, a UI kit), as long as the request carries email, message and the required empty company_website check. Name and subject are optional; MailContact.send() handles automatic verification in a custom interface.

related

Also works with

One inbox for every React project you ship.

Forms and a real email address, together, with notifications where you live. Start with a free account.

Create your free form

No credit card needed. Free includes one project and 50 form messages per month.