HTML can't send an email. The <form> tag never had that power. All a form does is collect its fields and hand them to whatever its action points at, and that receiver is the thing that sends the mail. So "without a backend" really means "without a backend you build and run yourself."
There are three honest ways to get there. A mailto: action, which you should not use. Google Apps Script, free and genuinely serverless but fiddly. And a hosted form endpoint, where you paste one URL and are done.
The short version: the hosted endpoint wins for anything you'd be upset to lose, because it's the only route that stores every submission and filters spam without you building either. Point your form at a Form Plume endpoint and skip to the code.
I build Form Plume, so treat that as the pitch it is. I'll still tell you where the free Apps Script route is the better call, because a post that pretends the paid option always wins is useless to you.
Why HTML can't email on its own
A form is a data-collection widget, nothing more. When you submit it, the browser bundles every named field and sends an HTTP request to the URL in the action attribute, using the method you set. That's the whole job.
Nothing in that flow touches email. There is no SMTP in the browser, no mail server, no send() anywhere in the HTML spec. The email only happens if the thing on the other end of the action decides to send one.
So the real question was never "how do I make HTML send email." It's "what do I put in the action so a submission lands in my inbox." Everything below is a different answer to that one question.
The mailto trap
The oldest tutorials tell you to skip the server entirely and do this:
<!-- Looks clever. Does not work. -->
<form action="mailto:you@site.com" method="post">It looks like it should send you an email. It doesn't. A mailto: action doesn't submit the data anywhere; it asks the visitor's browser to open their desktop mail client with your fields crammed into a draft.

Three things go wrong, every time. On a phone, a Chromebook, or any machine with no mail client set up, clicking submit does nothing at all, and the visitor has no idea their message went nowhere. When it does open a client, the field data arrives URL-encoded and ugly, and you can't control the formatting. And your address sits in the page source in plain text, which is exactly how scrapers build spam lists.
Nothing in the flow is guaranteed. There's no promise the visitor has a mail client configured, and no promise the opened draft ever gets sent. Treat mailto: as a way to link to an email address, never as a way to receive a form.
The DIY routes, and where they bite
The next thing tutorials reach for is server code. Two flavors show up, and both quietly break the "no backend" promise.
PHP mail() is the classic, and it's a trap on two levels. It needs a PHP server, so it isn't backend-free to begin with. And the function itself barely works in practice: some hosts disable it outright, and even where it runs, the mail lands in spam because it has no authenticated sender. The tutorials that start with mail() almost always end with "now install PHPMailer and configure SMTP," which is the backend you were trying to avoid.

A serverless function is the modern version of the same move. A Netlify, Vercel, or Lambda function that takes the POST and calls an email API does work, and it works well. But be honest about what it is: you're writing, deploying, and maintaining a backend. It has no persistent server to babysit, which is genuinely nice, but you still own the code, the API keys, the error handling, and the spam problem. If that's the project you want, great. If you came here to avoid writing a backend, this isn't it.
The reason both bite is the same one nobody puts on the tutorial: deliverability is a full-time job. SPF, DKIM, DMARC, sender reputation, warmup. Get one wrong and Gmail files your form notifications under spam, silently, and you find out weeks later when a lead complains you never replied. That warmed-sender problem is exactly the thing a hosted endpoint has already solved, so you inherit deliverability instead of earning it.
Google Apps Script: free, serverless, and fiddly
This is the one route that keeps its promise and costs nothing. You write a small script bound to a Google Sheet, deploy it as a web app, and POST your form to the URL Google gives you. The whole receiver is about ten lines:
// Bound to a Google Sheet, deployed as a web app.
function doPost(e) {
const data = e.parameter;
SpreadsheetApp.getActiveSheet()
.appendRow([new Date(), data.name, data.email, data.message]);
MailApp.sendEmail("you@site.com", "New form submission", data.message);
return ContentService.createTextOutput("ok");
}Each submission lands a row in the sheet and an email in your inbox, sent from Google so it actually arrives. No server, no card.
I recommend it honestly, for the right project. For a personal site or a side thing where a lost message costs nothing, it's hard to beat free.
The catch is that it's brittle in ways that eat an afternoon:
- The setup is long and the UI drifts. Deploy as web app, set "execute as me," set "anyone can access," then click past the full-page
Google hasn't verified this appwarning to authorize the scopes. Google reshuffles that flow often enough that most step-by-step guides are half wrong by the time you find them. - There's no spam protection. The script accepts anything that POSTs to it, so you're bolting on your own honeypot or reCAPTCHA before the sheet fills with junk.
- Gmail has daily sending limits, and a form that suddenly gets popular can hit them and go quiet.
- You maintain it. It's your script, your auth, your problem when a scope changes.
It's the best free DIY option, as long as "free" is worth the fiddling to you. But look at what you're rebuilding by hand: a stored dashboard, real spam filtering, warmed deliverability. A hosted endpoint like Form Plume ships all three on day one, so when the fiddling stops being worth it, that's the route that takes the work off your hands.
The shortcut: point the action at a form endpoint
A hosted form backend is a URL that already knows how to receive a POST, store it, filter spam, and email you. You don't write or run anything. You paste the URL into your form's action and ship:
<form action="https://api.formplume.com/f/your-form-id" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required>
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Send message</button>
</form>That's the entire integration. No JavaScript, no server, no SMTP. The browser POSTs the fields, Form Plume stores the submission in a dashboard, runs it through spam filtering, and sends the notification email from a warmed sender so it actually arrives. Every field needs a name attribute, since the browser only submits named fields; an input with just an id sends nothing.
This is the category the SERP is full of: Formspree, Web3Forms, and the rest all do a version of it, and I'll be straight that Form Plume is one of them. Where it pulls ahead: the free tier stores 500 submissions a month in a real dashboard instead of only relaying them to your inbox, and spam filtering, file uploads, and a signed webhook are on by default rather than bolted on later. If you want the honest free-tier comparison across all of them, I keep a roundup that names where each one still wins. Otherwise the pattern is the same everywhere: swap one URL and you're receiving mail.
Which route should you actually pick
Read it by what you're building, not by what's newest:
| Route | Actually sends? | Server to run? | Spam handling | Stored copy | Best for |
|---|---|---|---|---|---|
mailto: | No | None | None | None | Nothing. Don't. |
PHP mail() | Barely | Yes | You build it | You build it | Legacy hosts you're stuck with |
| Serverless function | Yes | None, you write code | You build it | You build it | Devs who want full control |
| Google Apps Script | Yes | None | You bolt it on | Google Sheet | Free personal projects |
| Hosted endpoint | Yes | None | Built in | Dashboard | Anything with a form you care about |
The split that matters most is the last two columns. A mailto: or bare email relay gives you no stored copy, so a lost email is a lost lead. For a static hobby site that's fine. For anything with revenue attached, I want the submission saved the second it arrives, whether or not the email ever shows up. Only the bottom row does that with nothing to build, which is why a hosted endpoint is what I reach for on any form I actually care about.
Before you rely on it
Whichever route you pick, three things save you a silent failure:
- Send one real submission from the deployed site and confirm it lands in your inbox. A form that "works" locally and a green 200 in the Network tab are not proof the email arrived. If it doesn't, my contact form debugging checklist walks the usual causes.
- Add a honeypot field before you publish, or the spam bots will find the form within days. Form Plume includes one, and the docs cover the pattern if you're rolling your own.
- Keep the styling and markup yours. The whole point of a form endpoint over a form builder is that you own the HTML; the HTML contact form guide has the full field setup, and there are matching guides for React and plain JavaScript if you submit with
fetch.
