Code

HTML contact form code

Copy-paste contact form code that sends the message somewhere, with every attribute that matters explained. Six blocks: plain HTML, a fetch version, the honeypot, a file upload, an accessible version and a whole contact us page.

Short answer: A working HTML contact form needs four things: an action that points to a real endpoint, method POST, a name on every field, and a label for every field. The code here posts to Formgong. Replace fk_your_access_key with your free key and messages reach your inbox. Add type=email, required and autocomplete, and a hidden honeypot for spam.

Plain HTML contact form code

The shortest form that actually delivers a message. No JavaScript, no framework, no server of your own. Paste it into any HTML page, a WordPress Custom HTML block, or a Webflow or Framer embed.

Before it works, replace fk_your_access_key with the key of a free Formgong form. Then send yourself a test. When someone presses Send, the browser posts the fields to Formgong. Formgong saves the message, emails you, and only then shows the visitor a thank-you page. If the key is wrong or a check fails, the visitor sees an error page that names the problem rather than a fake "Thanks".

contact-form.html
<form action="https://formgong.com/submit" method="POST">
  <input type="hidden" name="access_key" value="fk_your_access_key">

  <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="6" required></textarea>

  <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
    <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
  </div>

  <button type="submit">Send</button>
</form>

The attributes that make it work

  • action is where the data goes. Leave it out, or write action="#", and the form posts back to your own page, which for a static site means nowhere. Here it is Formgong's /submit address.
  • method="POST" sends the fields in the request body. Without it the browser uses GET and puts the whole message into the address bar, the browser history and the server logs.
  • name on every field. The browser sends only fields that have a name. An input with an id and a placeholder but no name looks fine and is silently dropped.
  • <label for> matching the input's id, or a label wrapped around the input. Screen readers read it, and a click on the text focuses the field. A placeholder is not a label: it vanishes as soon as someone types.
  • type="email" makes the browser reject an address without an @ and shows the email keyboard on phones.
  • required stops the submit while the field is empty. Put it on the fields you cannot use without. Watch out for <textarea required> </textarea>: the space between the tags counts as content, so the empty-looking box passes.
  • autocomplete (name, email, tel, organization) lets the browser fill in what it already knows. Fewer typos in the one field you need to reply.
  • access_key is a hidden input that tells Formgong which form this is. It is meant to be public. To stop other sites from posting with it, list your own domains in the form settings.

Formgong uses the field named email as the reply-to address of the notification, so Reply in your mail app answers the visitor. A hidden _subject input sets the subject line, and a hidden _redirect sends people to your own thank-you page (it has to be on a domain you added to the form).

Contact form with fetch: stay on the page

Same form, but the visitor never leaves your page. The script sends the fields with fetch and asks for JSON. Formgong answers with success: true or success: false and a message.

The important line is if (data.success === true). Only then does the form say thanks. A refused submission shows Formgong's reason, such as a wrong key, and a network failure says the message was not sent. Plenty of tutorials print "Thank you" before any request leaves the browser. A visitor who sees that has no reason to try again, and you never hear from them.

Do not set a Content-Type header yourself. Passing FormData lets the browser write the right one, and that matters once you add a file field.

contact-form-fetch.html
<form id="contact" action="https://formgong.com/submit" method="POST">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <label for="c-name">Name</label>
  <input id="c-name" name="name" autocomplete="name" required>
  <label for="c-email">Email</label>
  <input id="c-email" type="email" name="email" autocomplete="email" required>
  <label for="c-message">Message</label>
  <textarea id="c-message" name="message" rows="6" required></textarea>
  <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
    <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
  </div>
  <button type="submit">Send</button>
  <p id="contact-status" role="status" tabindex="-1"></p>
</form>

<script>
const form = document.getElementById("contact");
const statusLine = document.getElementById("contact-status");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button[type=submit]");
  button.disabled = true;
  statusLine.textContent = "Sending…";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
      headers: { Accept: "application/json" },
    });
    const data = await response.json();
    if (data.success === true) {
      form.reset();
      statusLine.textContent = "Thanks. Your message arrived.";
    } else {
      statusLine.textContent = data.message || "Not sent. Please try again.";
    }
  } catch {
    statusLine.textContent = "Not sent: check your connection and try again.";
  }
  button.disabled = false;
  statusLine.focus();
});
</script>

Honeypot: spam protection without a CAPTCHA

A honeypot is a field people cannot see and bots fill in. Simple bots read the HTML and put text into every input they find. Formgong treats a submission with text in botcheck as spam and keeps it out of your notifications.

Three details keep it working. It is moved off-screen with CSS rather than type="hidden", which bots know to skip. aria-hidden="true" and tabindex="-1" keep screen readers and keyboard users out of it. autocomplete="off" stops the browser from filling it for a real person.

It will not stop a person paid to spam you, or a bot that runs JavaScript. If those get through, turn on Cloudflare Turnstile in the form settings; most visitors never see a puzzle. More in how a honeypot field works.

honeypot.html
<!-- Put this inside your <form>, before the submit button. -->
<div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
    <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
  </div>

Contact form with file upload

A file field needs enctype="multipart/form-data" on the form. Without it, the browser sends the file's name and drops the file. accept only filters the file picker; the real type check happens on the server, on the bytes.

On Formgong, file posts need three things in the form settings: file attachments turned on, a verified email, and Cloudflare Turnstile. A post with files and no Turnstile answer is refused. Accepted types are PDF, PNG and JPEG: up to 3 files, 5 MiB each and 10 MiB together. Replace YOUR_TURNSTILE_SITE_KEY with your site key. The file upload guide covers the error codes and what each channel receives.

contact-form-upload.html
<form action="https://formgong.com/submit" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <label for="u-name">Name</label>
  <input id="u-name" name="name" autocomplete="name" required>
  <label for="u-email">Email</label>
  <input id="u-email" type="email" name="email" autocomplete="email" required>
  <label for="u-message">Message</label>
  <textarea id="u-message" name="message" rows="6" required></textarea>
  <label for="u-file">Files (PDF, PNG or JPEG, up to 3)</label>
  <input id="u-file" type="file" name="attachment" accept=".pdf,.png,.jpg,.jpeg" multiple>
  <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
    <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
  </div>
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
  <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>
  <button type="submit">Send</button>
</form>

Accessible version: labels, hints and autocomplete

This one adds what audits usually ask for. Every input has a visible label tied to it with for and id. The email hint is linked with aria-describedby, so a screen reader reads it after the label. Optional fields say so in the label, which is clearer than marking required ones with a red star alone.

The autocomplete values come from the HTML standard's list: name, email, tel, organization. Use those exact words. A made-up value is ignored. inputmode="email" is a small extra for phone keyboards on older browsers.

The browser's own validation messages, from required and type="email", are announced by screen readers, so you do not need a custom error script for a form this size.

contact-form-accessible.html
<form action="https://formgong.com/submit" method="POST" aria-describedby="form-note">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <p id="form-note">All fields are required unless marked optional.</p>

  <label for="a-name">Full name</label>
  <input id="a-name" name="name" autocomplete="name" required>

  <label for="a-email">Email</label>
  <input id="a-email" type="email" name="email" autocomplete="email" inputmode="email" required
         aria-describedby="a-email-hint">
  <small id="a-email-hint">We reply to this address.</small>

  <label for="a-phone">Phone (optional)</label>
  <input id="a-phone" type="tel" name="phone" autocomplete="tel">

  <label for="a-company">Company (optional)</label>
  <input id="a-company" name="company" autocomplete="organization">

  <label for="a-message">Message</label>
  <textarea id="a-message" name="message" rows="6" required></textarea>

  <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
    <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
  </div>

  <button type="submit">Send message</button>
</form>

Contact us page HTML: the whole file

A complete contact us page you can save as contact.html and upload as it is: the head with a title and description, a short intro, a direct email address for people who would rather not use forms, and the form. The CSS sits in the head and is about ten lines. Change the name, the email address and the colors, then add your key.

If you host on GitHub Pages, Netlify, Cloudflare Pages or any other static host, this is all you need: the page stays static and Formgong receives the messages.

contact.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contact us | Example Studio</title>
  <meta name="description" content="Write to Example Studio. We answer every message.">
  <style>
    body { margin: 0; font: 17px/1.6 system-ui, sans-serif; color: #1f2933; background: #f7f7f8; }
    main { max-width: 36rem; margin: 0 auto; padding: 48px 20px; }
    form { position: relative; display: grid; gap: 8px; }
    label { margin-top: 10px; font-weight: 600; }
    input, textarea { font: inherit; padding: 10px 12px; border: 1px solid #9aa5b1; border-radius: 8px; background: #fff; }
    textarea { min-height: 140px; resize: vertical; }
    button { margin-top: 14px; justify-self: start; padding: 12px 22px; border: 0; border-radius: 8px; background: #2f5bea; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
    :focus-visible { outline: 2px solid #2f5bea; outline-offset: 2px; }
  </style>
</head>
<body>
  <main>
    <h1>Contact us</h1>
    <p>Questions, quotes, a problem with an order: write below. You can also email hello@example.com.</p>
    <form action="https://formgong.com/submit" method="POST">
      <input type="hidden" name="access_key" value="fk_your_access_key">
      <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" required></textarea>
      <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
        <label>Leave this field empty <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
      </div>
      <button type="submit">Send</button>
    </form>
  </main>
</body>
</html>

Mistakes we found in popular snippets

On 13 October 2026 we read the top results for four searches: "html contact form code", "html contact form", "contact form html css" and "simple contact form html". We took the first 10 pages that offer form code to copy. One of them was down, so 9 were judged, by reading the code only.

  • 4 of 9 do not send anywhere. The form has no action, action="#", or a script that never makes a request. 3 of those 4 pages do not mention it. Of the 5 that do send, 3 come from form-backend companies and need their service.
  • No snippet has autocomplete on the name or email field: 0 of 9.
  • No snippet has spam protection in the code: no honeypot, no CAPTCHA, no check in the PHP. 0 of 9.
  • 5 of 9 leave at least one field without a working label: no label at all, a placeholder in its place, or a for that points to an input without that id.
  • 5 of 9 miss required on at least one of name, email and message. 2 of 9 leave the email field as a plain text input.
  • Fields without a name. In one snippet the name, email and phone inputs have no name, so only a phone code and the message would ever be sent.
  • A required message that is never empty. One textarea is written as required> </textarea>. The space counts as content, so an empty message goes through.
  • A missing closing tag. As shown on its page, one snippet never closes its <textarea>. The browser reads the Send button as text inside the box, and the button disappears.
  • "Thank you" without a request. The only snippet with JavaScript logs the fields to the console and shows a thank-you alert. Nothing is sent.
  • PHP that rejects real people. One PHP handler refuses addresses with a plus sign or a long domain ending such as .agency, and names like José García. Another copies every message to an example.com address.

Not one of the 9 passed all of our checks. The code on this page was written against the same list: a working action, POST, names and labels on every field, type="email", required, autocomplete and a honeypot. The form checker runs similar checks on a published page.

Questions

Is this HTML contact form code free?

Yes. Copy it without an account. To receive messages, create a free Formgong form and replace fk_your_access_key with its key.

Can an HTML form send email without PHP?

Not by itself. HTML only sends the fields to an address. Something at that address has to store them and send the email. Here that is Formgong, so your page needs no PHP and no server of its own.

Why does my form send nothing?

Usually one of three things: no action or action="#", fields without a name attribute, or a script that shows thanks without making a request. The form checker finds the first two on a published page.

What is the best way to stop contact form spam?

Start with the hidden honeypot field in this code. If bots still get through, turn on Cloudflare Turnstile in the Formgong form settings.

Where do I find ready designs?

The contact form examples page has fourteen styled forms with previews, from a simple form to a dark one, a quote request and a multi-step form.

Sources

Checked on 16.10.2026 against these public pages: MDN: <form>, MDN: <label>, MDN: autocomplete, HTML standard: autofill field names, MDN: Using FormData, Formgong HTML guide.

← Formgong