How to Make a Contact Form in HTML That Sends Emails

Quick answer

<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST"> <label for="email">Your email</label> <input type="email" id="email" name="email" required> <label for="message">Message</label> <textarea id="message" name="message" required></textarea> <button type="submit">Send</button> </form>

How a contact form works

When a visitor presses Send, the browser packages every field that has a name and sends it to the address in the form’s action attribute. Something at that address has to receive the data and send the email. That’s why a form copied into a plain HTML page “does nothing”: there’s no handler behind it.

You have four options for the handler:

  • A form service (Formspree, Web3Forms, Getform and similar): no server code, a free tier for small sites. Best for static HTML sites.
  • Your host’s built-in forms: Netlify, for example, detects forms with a data-netlify="true" attribute.
  • A WordPress plugin (Contact Form 7, WPForms, Fluent Forms) if your site runs on WordPress.
  • Your own server script in PHP, Node.js or Python, if you run your own server.

Step 1: Build the form fields

A contact form usually needs a name, an email address and a message. Keep it short: every extra field means fewer people finish it.

  1. Add <form method="POST">. Always use POST for messages, so they aren’t put in the URL.
  2. Give every field a <label>, an id and a name. The name becomes the field’s label in the email you receive.
  3. Use type="email" for the email field, and required on fields that must be filled in.
  4. Add autocomplete attributes (name, email) so browsers can fill them in.

The example has the full form with styling and built-in validation. Press Send with empty fields to see the browser’s messages. (In this preview, submitting just shows a note; it isn’t connected to a handler.)

index.html
Preview

Step 2: Connect a form service (easiest)

Form services give you a unique address to put in your form’s action. They receive each submission, filter spam, and email it to you. The steps are similar for all of them; with Formspree, for example:

  1. Create a free account and a new form, entering the email address where messages should go.
  2. Copy the endpoint it gives you, which looks like https://formspree.io/f/abcdwxyz.
  3. Paste it into your form: <form action="https://formspree.io/f/abcdwxyz" method="POST">.
  4. Publish the page and send yourself a test message. The first submission usually asks you to confirm your email address.

Web3Forms works slightly differently: the action is always https://api.web3forms.com/submit, and you add a hidden field with your key: <input type="hidden" name="access_key" value="YOUR_KEY">. Whichever you choose, check the free plan’s monthly limit and its privacy policy, since visitors’ messages pass through the service.

Other options: WordPress plugins and server scripts

  • WordPress: don’t paste a raw HTML form into a page. Install a form plugin such as Contact Form 7 or WPForms, build the form there, and place it with its shortcode or block. The plugin handles sending, spam filtering and storage. For reliable delivery, also set up an SMTP plugin so emails are sent through a real mail account.
  • Your own server: point action at a script such as /contact.php. The script must check every field again (never trust the browser), protect against spam, and send the email, ideally through an SMTP service rather than PHP’s basic mail() function, which often lands in spam folders.
  • Avoid action="mailto:…". It doesn’t send anything itself; it tries to open the visitor’s email app, fails for anyone without one set up, and exposes your address to spam bots.

Step 3: Protect the form from spam

Any public form attracts spam bots within days. Layer a few defences:

  • A honeypot field: an extra input that’s hidden from people but visible to bots in the code. Real visitors leave it empty; bots fill it in, and the handler discards those messages. Formspree, for example, uses a field named _gotcha.
  • The service’s own spam filter, which most form services and plugins turn on by default.
  • A CAPTCHA (such as Cloudflare Turnstile, hCaptcha or reCAPTCHA) if spam still gets through. Prefer invisible or low-friction ones; puzzles annoy real visitors.
<!-- honeypot: hidden from people, tempting for bots --> <div style="position: absolute; left: -9999px;" aria-hidden="true"> <label>Leave this empty <input type="text" name="_gotcha" tabindex="-1" autocomplete="off"></label> </div>

Step 4: Show a thank-you message

By default, submitting a form leaves your site for the service’s own “thanks” page. Two better options:

  1. Redirect to your own page: most services let you set a “redirect URL” (in their dashboard or with a hidden field) to a page like /thank-you/.
  2. Stay on the page: send the form with JavaScript’s fetch(), then replace the form with a message. Most services accept this when you send the header Accept: application/json.

The example below shows the stay-on-the-page pattern. In this preview it fakes the network request (a placeholder address can’t receive anything), but the structure is what you’d use on a real site.

index.html
Preview

Which form handler should I use?

HandlerSetupCostBest for
Form serviceMinutes; change the actionFree tier, then monthly plansStatic HTML sites
Host’s built-in formsAn attribute on the formUsually free up to a limitSites already on that host
WordPress pluginInstall and build the form in the pluginFree plugins availableWordPress sites
Own server scriptWrite and secure the code yourselfYour hostingDevelopers with a server
mailto: actionNoneFreeNot recommended

Common contact form mistakes

  • No handler. A form without a working action silently goes nowhere. Always send yourself a test message after publishing.
  • Missing name attributes. Fields without a name aren’t sent, so the email arrives empty.
  • Using GET. Messages end up in the URL and browser history. Use method="POST".
  • No spam protection. Add a honeypot or turn on the service’s filter from day one.
  • No confirmation. Visitors who see nothing happen will submit again, or give up. Show a clear thank-you message.
  • Emails landing in spam. Check your spam folder when testing, and use a proper sending setup (the service’s, or SMTP for your own script or WordPress).
  • Forgetting privacy. Mention in your privacy policy that the form collects names and emails, and who processes them.

Frequently asked questions

Can an HTML form send an email by itself?
No. HTML only collects the data and sends it to the address in the form's action attribute. A form service, a WordPress plugin or a server script has to receive the data and send the email.
How do I make a contact form without PHP?
Use a form service such as Formspree or Web3Forms. You create a form in their dashboard, copy the endpoint address into your form's action attribute, and they email each submission to you.
Why is my contact form not sending emails?
Usually the form has no working handler, its fields are missing name attributes, or the emails are landing in your spam folder. Send a test message and check the form service or plugin logs.
How do I stop spam on my contact form?
Add a hidden honeypot field that only bots fill in, enable your form service's or plugin's spam filter, and add a low-friction CAPTCHA such as Cloudflare Turnstile if spam still gets through.
Should I use mailto in a form action?
No. action="mailto:..." only tries to open the visitor's email app, which fails for many people, and it exposes your email address to spam bots. Use a form handler instead.
How do I add a contact form to a WordPress site?
Install a form plugin such as Contact Form 7 or WPForms, create the form in the plugin, and add it to a page with its block or shortcode. An SMTP plugin makes delivery more reliable.