Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. HTML forms, the complete beginner guide
Web Development

HTML forms, the complete beginner guide

HTML forms: a working one is nine lines. Here is that form, what the browser validates for free, and what decides whether a field reaches your server.

By Max Arthur
Co-Founder & Content Marketer·September 8, 2026·9 Min read
White text reading 'HTML forms explained' on a dark purple dotted background

An HTML form collects input and sends it somewhere. It needs a <form> with an action and a method, some inputs with name attributes, and a submit button:

<form action="/subscribe" method="post">
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />

  <button>Sign up</button>
</form>

That form works today, in every browser, with no JavaScript. It validates the email, refuses to submit when the field is empty, shows an error message you did not write, and posts the data.

Most of what beginners write JavaScript for is already in that snippet. Every behaviour described below was measured in a real browser using the form validation API, so the messages and results are the actual ones, not paraphrases.

The four attributes that decide everything

action is where the data goes. Leave it out and it posts to the current URL.

method is how. get puts the data in the URL as a query string, which is right for searches and filters because the result is a shareable link. post puts it in the request body, which is right for anything that changes something or is private. Passwords in a get form end up in the URL bar and in server logs.

name is the one people forget, and it is the one that matters most. A field without a name attribute is not submitted at all. Measured on a form with two text inputs, one named and one not, both filled in:

submitted keys: ["named"]

The unnamed field had a value and was silently dropped. So are disabled fields. If a value is not arriving at your server, check name before you check anything else.

id exists to connect the label. It is not what gets submitted.

Labels are not decoration

<label for="email">Email</label>
<input type="email" id="email" name="email" />

The for attribute must match the input's id. When it does, three things happen: screen readers announce the field's purpose, clicking the label focuses the input, and the tap target for checkboxes and radios becomes the whole label instead of a 13-pixel square.

That last one is a measurable usability difference on phones, and it costs nothing.

Wrapping also works and skips the id:

<label>Email <input type="email" name="email" /></label>

What does not work is a <div> styled to look like a label, or placeholder text used instead of one. Placeholders disappear the moment someone types, so a half-completed form becomes a set of unlabelled boxes, and they are not reliably announced by assistive technology.

Input types do real work

type changes validation, the mobile keyboard, and the control the browser renders.

type You get
text Plain text, no validation
email Format check, @ keyboard on mobile
password Masked characters
number Numeric keypad, min/max/step
tel Phone keypad, no format check
url Format check, URL keyboard
date A date picker
checkbox On or off, independent
radio One choice from a group sharing a name
file A file picker
search, color, range, time Specialised controls

Using type="email" instead of type="text" is a one-word change that adds validation and switches the phone keyboard. There is rarely a reason not to.

A caution on type="number". It is for genuine quantities. Do not use it for phone numbers, credit cards or postcodes: they are not arithmetic, leading zeros get eaten, and the spinner arrows make no sense. Use type="tel" and a pattern.

What the browser validates for free

Add required and the browser stops the submission and shows a message. The exact message, measured in Chromium:

valid: false
message: "Please fill out this field."
validity.valueMissing: true

Fill it with something that is not an email:

valid: false
message: "Please include an '@' in the email address. 'not-an-email' is missing an '@'."
validity.typeMismatch: true

The browser wrote a better error than most hand-rolled ones, in the visitor's own language, and it did not need a script.

min and max on a number field work the same way:

age = 9   -> "Value must be greater than or equal to 13."   rangeUnderflow: true
age = 200 -> "Value must be less than or equal to 120."     rangeOverflow: true

The complete set of constraint attributes:

Attribute Applies to Checks
required Nearly all Not empty
min / max Numbers, dates Within range
minlength / maxlength Text Character count
pattern Text Matches a regular expression
step Numbers, dates Valid increment

Two things about email validation worth knowing

It is looser than you think. Measured against the browser's own checker:

Value Valid?
test@example.com yes
a@b.c yes
a@b yes
no-at-sign.com no
spaces in@example.com no
a@@b.com no

a@b passes. The browser checks the shape, not whether the address exists or has a real domain. That is the correct trade, because the only way to truly validate an email address is to send mail to it. Use type="email" to catch typos, and confirm ownership with a verification link.

minlength behaves differently than you would expect. Setting a five-character value on a field with minlength="8" from JavaScript reported:

valid: true
validity.tooShort: false

Valid, despite being too short. This is intentional. MDN specifies that tooShort applies only to a value entered "after having been edited by the user". Values you set in code do not trigger it. A real person typing five characters is blocked normally, so this only bites when you prefill a form or write tests against one.

Whole-form validity, and the checkbox that blocks it

form.checkValidity() reports on every field at once. Measured with an unchecked required checkbox and every other field filled in correctly:

before checking the box: false
after checking the box:  true

One unchecked terms box invalidates the entire form. That is the behaviour you want, and it is why "the submit button does nothing" is usually a required field scrolled out of view rather than a broken handler.

What actually gets sent

Here is where forms surprise people. This form, fully filled in:

email=reader@example.com
pw=long-enough-password
age=30
site=
plan=pro
terms=on
contact=email
notes=

Four things to notice.

A checked checkbox sends on, not true. You chose no value, so the browser used its default. Set value="yes" if you want something specific.

An unchecked checkbox sends nothing at all. Measured on the same form with the box cleared:

keys: ["email", "pw", "age", "site", "plan", "contact", "notes"]

The terms key is simply absent. There is no terms=off. On the server, check for the key's presence, never for a falsy value.

Radio buttons send one value under the shared name. Three radios named contact produced one entry, contact=email, the checked one. That shared name is also what makes them mutually exclusive, so getting it wrong lets people select all of them.

Empty text fields still send an empty string. site= and notes= are present with no value. Absent and empty are different things, and only checkboxes and radios give you absent.

The button trap

Buttons inside a form default to type="submit". Measured on three buttons written as <button type="submit">, <button type="button"> and <button>:

["submit", "button", "submit"]

The third one, with no type at all, is a submit button. This is why adding a "Show password" or "Add another row" button to a form makes the page reload unexpectedly. It is submitting.

Always write type="button" on any button in a form that is not meant to submit.

Grouping, and the elements people never learn

<fieldset> and <legend> group related fields and give the group an accessible name:

<fieldset>
  <legend>Delivery address</legend>
  <label for="street">Street</label>
  <input id="street" name="street" />
</fieldset>

This matters most for radio groups. Without a <legend>, a screen reader announces "email, radio button, one of three" with no indication of what the question was.

Also worth knowing:

  • <select> with <option> children for a fixed list. Add multiple for several choices.
  • <textarea> for long text. Its value goes between the tags, not in a value attribute, which catches everyone once.
  • <datalist> for suggestions that do not restrict input.
  • autocomplete with the standard tokens (email, name, street-address, cc-number) lets browsers and password managers fill fields correctly. It is one attribute and a genuine improvement for anyone filling in the same details for the hundredth time.

Accessible by default, if you do four things

  1. Every input has a <label> with a matching for. Not a placeholder, not a nearby div.
  2. Group related controls in a <fieldset> with a <legend>.
  3. Do not rely on colour alone for errors. Red text needs words next to it.
  4. Keep the tab order matching the visual order. This is where CSS order and flex-direction: row-reverse cause real problems, since they move things visually while keyboard focus follows the HTML.

None of these cost development time if you do them while writing the markup. All of them are painful to retrofit.

Still validate on the server

Everything above happens in the visitor's browser, which means everything above can be switched off. Anyone can open DevTools, delete required, and submit. A request does not have to come from your form at all.

Browser validation is a convenience for honest users. Server validation is the actual rule. Do both: the browser gives instant feedback without a round trip, and the server is what protects your data.

Build one

Forms are the point where a page stops being a document and starts being an application, and they are the fastest way to feel that shift. The nine-line form at the top is genuinely all you need to start.

HTML Basics covers forms, labels and input types with the page rendering live beside your markup, so a required field blocking a submit is something you trigger rather than read about.

Related reading: CSS Flexbox for laying out form rows without margin hacks, How to Center a Div for centering the form card itself, and How Do Websites Work for what happens to that POST request after the button is clicked.

Keep reading

More from the blog

White text reading 'Why your z-index is not working' on a dark purple dotted background
September 17, 2026·7 min readWeb Development

z-index not working? It is the parent, not the number

A child with z-index 9999 loses to a sibling with z-index 1 when the parent has opacity. Same markup without it, and 9999 wins.

Read more
White text reading 'CSS variables explained' on a dark purple dotted background
September 16, 2026·10 min readWeb Development

CSS variables in practice: scope, fallbacks and theming

Custom properties inherit and cascade, which is what separates them from Sass variables. An invalid value falls back further than you expect.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons