A web page cannot send email by itself. The form has to post its data to something that can: a form endpoint service, your host's form feature, a serverless function that calls an email API, a Google Apps Script or your own server. Pick by how much control you need, then submit with JavaScript so visitors stay on the page.
| Method | Backend code | Cost to start | Best for |
|---|---|---|---|
| mailto: link or action | None | Free | Quick personal sites; unreliable for business |
| Form endpoint service | None | Free tier, then paid | Static sites that need it working today |
| Netlify Forms | None, on Netlify | Free tier, then paid | Sites already hosted on Netlify |
| Serverless function + email API | A small function | Free tiers usually cover small sites | Full control over format and routing |
| Google Apps Script | A short script | Free with a Google account | Low-volume forms that should land in Gmail and a Sheet |
Updated
Each method below has complete code. Start with the form and the JavaScript submit handler, which work with every method, then pick a backend.
This front end works with every method. The script sends the form with fetch, shows a status message and keeps the text if sending fails.
Labels wrap their inputs, the browser checks required fields and email format, and the status line is announced to screen readers.
<form id="contact" action="https://example.com/your-endpoint" method="POST">
<label>Name <input name="name" autocomplete="name" required></label>
<label>Email <input name="email" type="email" autocomplete="email" required></label>
<label>Message <textarea name="message" rows="4" required></textarea></label>
<!-- Honeypot: hidden from people, filled in by simple bots -->
<label class="hp" aria-hidden="true">Leave empty <input name="company" tabindex="-1" autocomplete="off"></label>
<button type="submit">Send</button>
<p id="status" role="status"></p>
</form>
<style>
#contact { display: grid; gap: 10px; max-width: 360px; font: 15px system-ui, sans-serif; }
#contact label { display: grid; gap: 4px; }
#contact input, #contact textarea { padding: 8px; border: 1px solid #ccc; border-radius: 8px; font: inherit; }
#contact button { padding: 10px; border: 0; border-radius: 8px; background: #111827; color: #fff; }
.hp { position: absolute; left: -9999px; }
</style>Works with any endpoint that accepts a normal form post. If the endpoint wants JSON, send JSON.stringify(Object.fromEntries(data)) with a JSON content type instead.
const form = document.getElementById("contact");
const status = document.getElementById("status");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = new FormData(form);
if (data.get("company")) return; // honeypot filled: quietly ignore
const button = form.querySelector("button");
button.disabled = true;
status.textContent = "Sending...";
try {
const res = await fetch(form.action, {
method: "POST",
body: data,
headers: { Accept: "application/json" },
});
if (!res.ok) throw new Error(res.statusText);
form.reset();
status.textContent = "Thanks, your message is on its way. We reply within two working days.";
} catch {
status.textContent = "Sorry, that did not send. Please try again or email us directly.";
} finally {
button.disabled = false;
}
});Opens the visitor's email app with the fields in the body. No server needed, but nothing is sent if they have no mail app.
<form action="mailto:[email protected]" method="post" enctype="text/plain">
<input name="name" placeholder="Name">
<textarea name="message" placeholder="Message"></textarea>
<button>Open email to send</button>
</form>Services such as Formspree, Basin, Getform and Web3Forms give you a URL to post to and forward each submission to your inbox. Sign up, create a form, paste its URL into the action attribute of the form above and keep the fetch script. Check each service's current free limits and where it stores data.
Netlify detects the form at deploy time when it has the data-netlify attribute. Turn on email notifications in the site's form settings. For JavaScript-rendered forms, also keep a plain HTML copy in your built files so Netlify can find it.
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="company">
<input type="hidden" name="form-name" value="contact">
<p class="hp"><label>Leave empty <input name="company"></label></p>
<label>Email <input type="email" name="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">Send</button>
</form>
<script>
// To submit without leaving the page, post URL-encoded data to "/":
// fetch("/", { method: "POST",
// headers: { "Content-Type": "application/x-www-form-urlencoded" },
// body: new URLSearchParams(new FormData(form)).toString() });
</script>The function receives the form, checks it, and calls an email API with a secret key kept on the server. This example uses Resend's REST API; SendGrid, Postmark and Amazon SES work the same way with their own endpoints.
Works on platforms that use the web Request and Response objects. Set RESEND_API_KEY in your host's environment variables and verify your sending domain with the email provider first.
export default async function handler(req) {
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
const form = await req.formData();
const name = String(form.get("name") || "").slice(0, 100);
const email = String(form.get("email") || "").slice(0, 200);
const message = String(form.get("message") || "").slice(0, 5000);
if (form.get("company")) return Response.json({ ok: true }); // honeypot
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || !message) {
return Response.json({ ok: false, error: "Missing or invalid fields" }, { status: 400 });
}
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Website <[email protected]>",
to: ["[email protected]"],
reply_to: email,
subject: `New message from ${name || email}`,
text: `From: ${name} <${email}>\n\n${message}`,
}),
});
return Response.json({ ok: res.ok }, { status: res.ok ? 200 : 502 });
}A short script, deployed as a web app from a Google account, emails each submission and can log it to a Sheet. Daily email quotas apply, so it suits low-volume forms.
In a new Apps Script project, paste this, then Deploy as a web app that runs as you and that anyone can access. Use the web app URL as the form action.
function doPost(e) {
const p = e.parameter;
if (p.company) return ContentService.createTextOutput("ok"); // honeypot
MailApp.sendEmail({
to: "[email protected]",
replyTo: p.email,
subject: "New website message from " + (p.name || p.email),
body: "From: " + p.name + " <" + p.email + ">\n\n" + p.message,
});
// Optional: keep a copy in a Sheet
// SpreadsheetApp.openById("SHEET_ID").getSheets()[0]
// .appendRow([new Date(), p.name, p.email, p.message]);
return ContentService.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}Shared hosting often runs PHP. The built-in mail() function works on many hosts, but deliverability is better with an SMTP library such as PHPMailer and a real mailbox on your domain.
Rejects non-POST requests, trims inputs, refuses line breaks in the reply address (which would let attackers add headers) and redirects to a thank-you page.
<?php
if ($_SERVER["REQUEST_METHOD"] !== "POST" || !empty($_POST["company"])) { http_response_code(400); exit; }
$name = trim(substr($_POST["name"] ?? "", 0, 100));
$email = trim($_POST["email"] ?? "");
$message = trim(substr($_POST["message"] ?? "", 0, 5000));
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match("/[\r\n]/", $email) || $message === "") {
http_response_code(400); exit("Please fill in every field.");
}
$headers = "From: Website <[email protected]>\r\nReply-To: $email";
mail("[email protected]", "New message from $name", "From: $name <$email>\n\n$message", $headers);
header("Location: /thanks.html", true, 303);
If the site is static and you want it working in ten minutes, use a form endpoint service or your host's built-in forms. If you already have serverless functions, an email API gives you full control over the message and costs little at small volumes. If the business runs on Google Workspace, Apps Script keeps everything inside that account at no extra cost.
Whatever you pick, the front end stays the same: a normal form plus the fetch handler below. Switching backends later means changing one URL.
A contact form collects personal data. Say what you do with it next to the submit button, link your privacy policy, and keep only what you need. If you use a third-party service, it processes that data for you, so check where it stores it.
Only with a mailto: action, which opens the visitor's own email app with the details filled in. It fails for anyone without a mail app set up, which includes many people on shared or work computers, and you cannot style or validate the result. Use one of the other methods for anything important.
Usually because the message claims to come from the visitor's address, or because your domain lacks SPF, DKIM and DMARC records for the service that sends it. Send from an address on your own domain, set the visitor as reply-to, and add the DNS records your email provider gives you.
A field hidden from people but visible to simple bots. Humans leave it empty; bots fill it in. Your backend discards any submission where it has a value. It stops a lot of basic spam without bothering real visitors.
Not at first. Start with a honeypot and server-side checks such as rejecting messages full of links. Add a challenge only if spam keeps coming, and prefer one that does not ask people to solve puzzles.
Related: How to add a contact form, Business email setup, Contact link generator, Booking form generator, Privacy policy generator.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.