HTML contact form, written in full: the contact form HTML a static site pastes in, and where it posts

Form controls the markup will hold
8
Label elements, one bound to each control
8
Fieldset and legend pairs around the choice groups
1

The counts these tools work out come from the structure the HTML standard defines for a form: one labelled control per field, a fieldset and a legend around a group of radios or checkboxes, and a name attribute on every control that is meant to be submitted. The sources under each tool link the part of the specification each rule comes from.

A contact form is the smallest useful form on the web and the one most often written slightly wrong. The markup below is the whole of it: a form element with a method and an action, a label bound to every control by its id, an email input so the browser checks the address before anything is sent, a textarea for the message and one submit button. What is left after that is the action attribute, which is where the browser posts to, and on a static site that is the part you do not have.

Open the HTML form generator Free to use. No account, no card, no sign-up.

Ask for four things, and resist the fifth

A name so you know who is writing, an email address so you can answer, an optional subject if it genuinely helps you triage, and the message. That is the complete list for almost every site. Every extra field costs you submissions, and the fields people add first are the ones they want rather than the ones the visitor came to give: a phone number nobody wanted to leave, a company name on a personal enquiry, a dropdown of departments the visitor cannot possibly map their question onto. Ask for less and answer faster.

How to create a contact form in HTML: write the form element first

Start with the container, because two of its attributes decide everything that follows. method="post" keeps what somebody typed out of the query string, out of browser history and out of your logs. action is the URL the browser sends the submission to, and on a static host there is nothing at your own domain that can receive it, so this attribute is the whole of the problem: it has to point at something that accepts a POST. Get those two right and the rest of the form is just fields.

One labeled control per thing you want to know

Every control gets its own label element, bound with for and id, and not a placeholder pretending to be one: a placeholder disappears the moment somebody types, which is exactly when they need to know what the field was. Choose the input type by what the answer is, because the type is what makes a phone show the right keyboard and the browser check the shape of an address. A message is a textarea, not a long text input. On the worked example beside this page, five separate questions come to 8 controls once the group of options is counted properly, and 8 label elements with them.

Decide what happens after the submit button

A form is finished when you know where the submission goes and what the person sees next. The browser will post to the action URL and then render whatever comes back, so something has to reply, and it has to reply with something a human can read. Required attributes will have caught the empty fields, but nothing has checked anything that matters yet, because the browser is not yours. Whatever receives the post has to validate it again, store it, and tell the person it arrived.

Know what required promises, and what it does not

Adding required to a control makes the browser refuse to submit while it is empty and show a message next to it, with no round trip and no JavaScript. That is a real improvement for the person filling the form in, and it is not a rule you can rely on. The browser belongs to them, not to you: developer tools can remove the attribute in a second, and nothing stops a script posting whatever it likes straight at your action URL. Treat every constraint attribute as a courtesy that saves a round trip, and check the same things again wherever the submission actually lands.

Point the action at something that can answer

This is the part the snippets skip. If your page is served from object storage or a static host, there is no process at your domain to receive a POST, so the action has to point somewhere that can: a serverless function you write and deploy, a small server of your own, or a hosted endpoint built for exactly this. Whichever you choose, it has to do three things: validate the submission again, do something durable with it so it is not only an email that can be lost, and reply with a page a human can read, because the browser will render whatever comes back.

Show the visitor a page that says it arrived

The browser posts and then renders whatever the action URL returns, so something has to answer with a page a human can read. A confirmation that says the message arrived and when to expect a reply is worth more than a clever animation, because the alternative is somebody who cannot tell whether it worked sending the same message three times. If the form submits with JavaScript instead, the same rule holds: say plainly that it arrived, and say what happens next.

Stop the spam without stopping the people

A public contact form gets automated submissions from the day it is indexed. Most of it is stopped by unremarkable measures: a hidden field that only a bot fills in, a check on how quickly the form was completed, rate limiting at the endpoint. Reach for a visible puzzle last, because every one of them costs real submissions from real people, and the ones it costs you most are exactly the visitors you were hoping to hear from.

Will the HTML contact form write the markup you need?

Tell us what the form has to do and we will tell you whether Endpointo can catch it.

Send it

Writing the HTML contact form: what developers ask

What goes in the action attribute if my site is static? An absolute URL that accepts a POST. A static host serves files and cannot receive one, so the action has to point somewhere else: a serverless function you write, your own server, or an endpoint service like Endpointo Pro. Leaving it empty posts the form back at the page it came from, which on a static host does nothing useful.

Do I need method="post" or will get do? Use post for a contact form. get puts every field in the query string, which means the message ends up in browser history, server logs and any referrer header, and it caps how much can be sent. get is right for a search box and wrong for anything a person typed privately.

Does the required attribute mean I can skip checking on the server? No. required stops most people submitting an empty field and gives them an instant message, which is worth having, but it is enforced by the browser and a browser is not something you control. Anything that decides what happens to the submission has to be checked again wherever the form posts to.

Should the action point at a mailto: address? No. A mailto action hands the submission to whatever mail client the person has configured, which on plenty of machines is nothing at all, and when it does work it opens a half-filled draft they still have to send themselves. It also puts your address in the page for every scraper to collect. Post to a URL that accepts the request instead.

Should the subject field be a dropdown? Only if you genuinely route on it. A dropdown of departments makes the visitor guess which part of your organization owns their problem, which is a question they cannot answer and you can. A free text subject, or none at all, is usually better.

Do I need a privacy note next to the button? A sentence saying what you do with the message and that you will not pass it on takes ten seconds to write and answers a question a lot of people are quietly asking. Whether more is required of you depends on where you and your visitors are, which is not something a template can decide.

Endpointo Pro

Where the submission goes next

The markup is only half a working form. Point its action at Endpointo and the posts start arriving in your inbox, with every submission and its uploads kept in one place you can search and export.

  • Download the files somebody uploaded with the form
  • Send notification emails without our name on them
  • Come back to a form and its submissions tomorrow
  • Put your own logo and reply-to address on the notifications
  • Export every submission a form has ever received
  • Send the notification email to more than one address
  • Connect Stripe when a form needs to take a payment
  • Push submissions into the tools you already run

$19per month, whole team

Start Endpointo Pro Free for 14 days. No card needed. Pricing

The first 14 days are free and no card is asked for. Add a card during the trial and the first charge is on the day it ends; do not, and your workspace becomes read-only with nothing deleted. Endpointo Pro is $19.00 per month. It renews automatically each month at the same price until you cancel, and the price and the renewal date are shown on the checkout page before you pay.

Get an endpoint for this formPoint this form at Endpointo