# How to create an HTML form, step by step

Build a contact form in plain HTML in five steps, with what the browser sent after each one, then make it deliver to your email without writing a server.

Author: Formgong
Published: 2026-11-06
Language: en
Canonical: https://formgong.com/en/blog/how-to-create-an-html-form/

Write a <form> element, put labelled fields inside it, and give every field a name attribute, because only named fields are sent. Set method="POST" and an action URL that will receive the data, add type and required for the browser's checks, and finish with a submit button. HTML alone can't store or email what is sent; the action has to point at a server, or at a form backend that emails it to you.

## What do you need before you start?

A text editor, Notepad on Windows, TextEdit in plain-text mode on a Mac, or Visual Studio Code, and a browser. Save the file as `contact.html` and open it by double-clicking. That's the whole setup for the first four steps.

The fifth step needs somewhere to send the form, because a browser can show a form but can't keep what people type into it. We come to that at the end.

## Step 1: the form and its fields

A form is a `<form>` element with fields inside it. `<input>` is a one-line field, `<textarea>` is a box for longer text, and `<button>` inside a form sends it. Here is the smallest page with a contact form on it.

It looks like a form and you can type in it. On 6 November 2026 we filled it in Chromium and clicked Send. Nothing was sent: the page reloaded with an empty address bar query and the fields empty. The browser had nothing to send, because none of the fields had a name.

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contact us</title>
</head>
<body>
  <h1>Contact us</h1>
  <form>
    <input>
    <input>
    <textarea></textarea>
    <button>Send</button>
  </form>
</body>
</html>
```

## Step 2: give every field a name

The `name` attribute is the label of the value in the data the browser sends. Without it, a field is left out, however much is typed in it. Add `name="name"`, `name="email"` and `name="message"` to the three fields and try again.

This time something was sent, but into the address bar. Our page reloaded as `contact.html?name=Ana&email=ana%40example.com&message=Hello+there`. That is the default method, GET, which puts the form into the URL. It suits a search box. For a message it is wrong: the text ends up in browser history and server logs, and long messages get cut off.

## Step 3: choose where it goes and how

Two attributes on the form decide that. `method="POST"` sends the data in the body of the request instead of the URL. `action` is the address that receives it. With both set, our test server received a POST with the body `name=Ana&email=ana%40example.com&message=Hello+there`, and the address bar stayed clean.

Which address to put in `action` is the question the rest of this article answers. Our guide to the [form action attribute](/en/blog/html-form-action/) covers the URLs that work and the ones that don't, and [how HTML forms work](/en/blog/how-html-forms-work-low-level/) shows the full HTTP request if you're curious what travels over the wire.

## Step 4: labels and the browser's checks

Put a `<label>` in front of each field and connect the two with `for` and `id`. Screen readers read the label when the field is focused, and clicking the label puts the cursor in the field; in our test a click on “Email” focused the email box. Wrapping the field inside the label, `<label>Email <input name="email"></label>`, does the same without the `id`; a click on the wrapping label's text focused the field too.

A placeholder is not a label. It disappears as soon as someone types, so they can't check what the field was for, and it is never sent: a field showing only its placeholder arrived as an empty `name=` in our test. Keep a visible label and use the placeholder, if at all, for an example of the format.

Then let the browser check the input. `type="email"` tells it the field holds an email address, and `required` says it can't be empty. We left the name empty, typed “ana” as the email and clicked Send. Chromium refused and showed its own messages:

- Name: “Please fill out this field.”

- Email: “Please include an '@' in the email address. 'ana' is missing an '@'.”

Nothing was sent until both were fixed. A few more attributes cover most other checks. `minlength="20"` on the message gave “Please lengthen this text to 20 characters or more (you are currently using 9 characters).” `maxlength="5"` doesn't show a message at all; the browser simply stopped accepting keys after the fifth character. `pattern="[0-9]{5}"` on a ZIP code said only “Please match the requested format.”, which tells the visitor nothing, so describe the format in the label. When you want your own wording, or checks as people type, our guide to [JavaScript form validation](/en/blog/javascript-form-validation/) picks up from here.

Other useful types are `tel`, `url`, `number` and `date`; our article on [HTML input types](/en/blog/html-input-types/) shows what each one sends. Add `autocomplete="name"` and `autocomplete="email"` too, so the browser can fill them in.

## Step 5: make the form deliver

Now the form is right, and it still has nowhere to go. HTML has no way to save a message or send an email by itself, so `action` has to point at a program that does. You have two options.

**Write a small server** in PHP, Node or Python that reads the POST, stores or emails it, and blocks spam. It works, but you now run and maintain a server, mail delivery included, which is where most homemade forms quietly fail.

**Use a form backend.** We make Formgong, which is one of these. You put its address in `action` with your access key in a hidden field, and each submission arrives in your email, and in Telegram if you like. The visitor is sent to a thank-you page with a `303` redirect, or to your own page if you set one. The finished form below is the five steps plus that hidden field and a `botcheck` field that people never see and simple bots fill in. Formgong marks anything that arrives with it filled as spam.

The one rule to remember from step 2 still holds: Formgong, like any backend, only gets the fields that have a `name`, up to 50 of them per submission.

```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" name="email" type="email" autocomplete="email" required>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <!-- a field people never see; bots fill it in -->
  <input type="text" name="botcheck" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px" aria-hidden="true">

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

<style>
  form { display: grid; gap: .5rem; max-width: 28rem; font: 16px/1.5 system-ui, sans-serif; }
  input, textarea, button { font: inherit; padding: .6rem .7rem; border: 1px solid #b4bdce; border-radius: 6px; }
  textarea { resize: vertical; }
  label { margin-top: .5rem; font-weight: 600; }
  button { margin-top: .75rem; background: #1d4ed8; color: #fff; border: 0; cursor: pointer; }
</style>
```

## How do you add dropdowns, checkboxes and file uploads?

The same rule applies to every field: it needs a `name`, and what is sent is that name with a value. We added the common extra fields to a test form and looked at what the browser sent.

- **Radio buttons** for one choice out of several share one `name`, each with its own `value`. Ours sent `plan=pro`. Add `checked` to the one that should be picked at the start.

- **Checkboxes** are only sent when ticked. An unticked one was missing from the data entirely, and a ticked one without a `value` arrived as `news=on`, so give each a value that will make sense in the email.

- **A dropdown** is a `<select>` with `<option>` elements, and the chosen option's `value` is sent: `topic=support`. To make people choose instead of accepting the first option, start with `<option value="">Choose a topic</option>` and add `required`; Chromium then refused to send and said “Please select an item in the list.”

- **Groups** of related fields go in a `<fieldset>` with a `<legend>` as their title, which screen readers announce. A fieldset with `disabled` switches its fields off, and in our test those fields weren't sent at all.

- **A file upload** is `<input type="file">`, and the form needs `enctype="multipart/form-data"`. Without it our test server received only the file's name, `cv=cv.txt`; with it, the file itself arrived. The `accept` attribute only filters the file picker, so the receiving server has to check the files again.

Buttons have types as well. A `<button>` inside a form sends it unless you say otherwise; `type="button"` makes one that does nothing on its own (ours sent nothing), and `type="reset"` puts every field back to its starting value. A Clear button sits right next to Send and wipes everything with one wrong click, so most contact forms are better without one. Each of these fields has its own article here: [radio buttons](/en/blog/html-radio-button/), [checkboxes](/en/blog/html-checkbox/), [dropdowns](/en/blog/html-dropdown-menu/), [fieldset](/en/blog/html-fieldset/) and [the file input](/en/blog/html-file-input/).

If the form goes to Formgong, files work on every plan: up to three per submission, 5 MiB each and 10 MiB together, and a form that sends files has to include a Turnstile check, which keeps bots from using it to upload junk. Our [contact form with file upload](/en/blog/contact-form-with-file-upload/) has the complete code. If you collect personal data from people in the EU, also say under the form what you do with it; [which forms need what under GDPR](/en/blog/gdpr-form-types/) has notice texts to copy.

## Test it before you publish

Send yourself one real message from the published page, on a phone as well as a computer, and check that it arrives and doesn't land in spam. Then try it empty, and with a bad email, to see the browser's messages. Our checklist for [testing a contact form](/en/blog/how-to-test-a-contact-form/) goes through the rest, and if the button does nothing, [HTML form not submitting](/en/blog/html-form-not-submitting/) covers the usual causes.

Once the form works, the same Formgong account can add an AI help chat that answers visitors' questions on the page and a reviews widget that shows what customers said, without another script to look after.

## Where did you get stuck?

If one of these steps didn't work the way we described, or you'd like a step we left out, such as a dropdown or a file upload, tell us at [support@formgong.com](mailto:support@formgong.com). We will add it to this guide.

## Frequently asked questions

### How do I create a simple HTML form?

Use a <form> element with method="POST" and an action URL, add <input> and <textarea> fields that each have a name, connect each to a <label>, and add a submit button.

### Why is my HTML form not sending any data?

Usually the fields have no name attribute. Only named fields are sent; in our test a form without names sent nothing at all.

### Can an HTML form send an email without a server?

Not by itself. The action has to point at something that sends the email: your own server script or a form backend such as Formgong.

### Do HTML forms need JavaScript?

No. A form with a name on every field, method="POST" and an action sends without any script. JavaScript is only needed for extras, such as showing the thank-you message without leaving the page.

### Should a form use GET or POST?

POST for anything a person writes. GET puts the data in the address bar, which suits search boxes, not messages.

## Sources and documentation


- [HTML forms](https://formgong.com/en/docs/html/)

[Get a form key](https://formgong.com/en/#top)
