HTML basics · 8 / 8
lesson 8

Forms that work

Inputs, labels, validation, submission — the bits browsers do for free, and the bits you have to wire up.

~ 14 min read·lesson 8 of 8
0 / 8

A login form, a search box, a checkout — every interactive page on the web is built on HTML forms. The good news: browsers do an enormous amount of form work for you. Keyboard navigation, validation, error messages, accessibility, autocomplete — all of it ships in the box. The bad news: that machinery only kicks in if you wire your form up the way the spec expects.

This lesson walks through what a form needs to be useful straight from HTML, with no JavaScript at all. Every concept here is something the browser will do for you for free if you ask correctly.

The shape of a form

A form is a <form> element wrapping one or more controls — inputs, dropdowns, buttons. The form has two attributes that decide what happens when the user submits it.

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

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

Reading top to bottom: the form posts to /subscribe (the action is the URL the data is sent to). The method is "post", which tells the browser to put the form data in the body of the request. There is one input — an email field with a label — and a submit button.

Click the button, and the browser packages up everything inside the form, sends it to /subscribe, and then loads whatever the server sends back. No JavaScript involved. This is progressive enhancement — the page works on its own; you can add JS later for fancier behaviour without losing the baseline.

A useful picture: a form is like a paper form on a clipboard. The <form> is the clipboard. The inputs are the lines you fill in. The submit button is the box at the bottom that says "drop in mailbox X". The action is the address on the mailbox; the method is whether the form is mailed (post) or shouted across the room (get).

The two methods, briefly: method="get" puts the form data in the URL — visible, bookmarkable, fine for searches. method="post" puts the data in the request body — hidden from the URL, used for anything that changes something on the server (signups, posts, payments).

Labels matter

A <label> is the text that names a control. Every input should have one, even if your design hides it visually. The label is what a screen reader announces when it lands on the input. It is also what makes the click target bigger — clicking the label focuses the input.

There are two ways to attach a label to an input. Both work; one is slightly more flexible.

labels.html
<!-- Pattern 1: explicit, using for + id -->
<label for="email">Email</label>
<input id="email" name="email" type="email">

<!-- Pattern 2: implicit, by wrapping -->
<label>
Email
<input name="email" type="email">
</label>

Pattern 1 connects the label to the input through matching for and id values. The label's for says "I belong to the input with id 'email'". This pattern is more flexible because the label and the input do not have to be neighbours in the markup — useful when CSS layouts split them.

Pattern 2 wraps the input inside the label. No ids are needed; the connection is the nesting itself. This pattern is shorter, but you cannot easily put the label and input in different parts of a CSS grid.

Pick whichever fits your layout and stay consistent.

broken-label.html
<!-- broken: label is not connected to anything -->
<div>Email</div>
<input name="email" type="email">

That looks identical on the page — text above an input — but a screen reader will announce the input as "edit, blank" with no clue what it is for. The same mistake happens when you write a <label> with no for and the input is its sibling rather than its child. The connection has to be explicit; the visual proximity is not enough.

Watch out

placeholder is not a label. Placeholder text vanishes the moment you start typing, leaves no name behind for screen readers, and is often invisible to users with colour-vision differences. Always pair an input with a real <label>; use placeholder only for an example value, not the field's name.

check your understanding
You design a search box where, to save space, you only show a placeholder ("Search...") and no visible label. The simplest accessible fix is:

Input types pull their weight

The type attribute on an <input> is doing more work than it looks like. Different types give you different keyboards on mobile, different built-in validation, and different defaults. Picking the right type is one of the highest-payoff decisions in form design.

A short tour of the most useful types:

  • text — plain text, the default.
  • email — text, but the browser checks it looks like an email address, and mobile keyboards add the @ key.
  • tel — a phone number; mobile keyboards switch to the dial-pad layout.
  • url — a URL; mobile keyboards make / and .com easier to reach.
  • number — a numeric value; the keyboard becomes numeric, and you can set min, max, and step.
  • date — a date; the browser renders a calendar picker.
  • password — masks the typed characters as dots.
  • search — a search box; some browsers add an "x" to clear it.
  • checkbox and radio — for boolean and one-of-many choices.
  • file — file upload.
  • hidden — invisible, used to ship along data the user did not type (like an item id).
types.html
<label>Email <input type="email" required></label>
<label>Phone <input type="tel"></label>
<label>Birthday <input type="date"></label>
<label>Quantity <input type="number" min="1" max="10" step="1"></label>

Reading the last one: the user can only type a number between 1 and 10, in steps of 1. The browser will refuse to submit the form if the value is out of range. On a phone, the keyboard becomes numeric — no need to hunt for the digits.

The cost of getting the type right is one word; the benefit is a measurably better experience on every device. Default to text only when none of the more specific types fit.

check your understanding
You add a "phone number" field to your contact form using <input type="text">. A user opens the form on their phone and gets the standard alphabet keyboard. What is the simplest fix?

Browser validation, for free

Browsers ship a small set of validation rules that run before the form submits. The rules are attributes you sprinkle on the inputs. If any rule fails, the browser shows a built-in error message and refuses to send the form.

The most useful ones:

  • required — the field cannot be empty. (Boolean attribute, from lesson 3 — present means on.)
  • minlength / maxlength — minimum and maximum number of characters.
  • min / max — minimum and maximum value (for numbers and dates).
  • pattern — a regular expression the value must match. (A regex is a tiny mini-language for matching strings; you do not need to know it for this lesson — just know pattern exists.)
  • type="email" and type="url" — mentioned above; the type itself is a validation rule.
validation.html
<form action="/signup" method="post">
<label>
  Username
  <input name="username" type="text" required minlength="3" maxlength="20">
</label>

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

<label>
  Age
  <input name="age" type="number" min="13" max="120" required>
</label>

<button type="submit">Sign up</button>
</form>

Try clicking submit with the form empty: each required field gets a "please fill out this field" message. Type "ab" in the username and the browser blocks submission with "please lengthen this text to 3 characters or more". Type "12" in the age and it complains about the min. None of this needs JavaScript.

What the browser does not do: check the data on the server. Anyone can bypass front-end validation by sending a request directly. So you still need to validate on the server. The browser rules are a UX nicety — fast feedback for honest users — not a security measure.

Tip

Required fields should be visually marked too — an asterisk, the word "required", or another non-colour cue. Browser validation kicks in after a submit attempt; the visible mark helps users plan as they fill the form.

check your understanding
Your signup form has type="email" required on the email field. A user submits without typing anything. What happens?

Submitting and the button

A submit button is the trigger for sending the form. The most reliable way to write one is a <button> with type="submit":

submit.html
<button type="submit">Sign up</button>

<button> elements default to type="submit" inside a form, so writing it explicitly is belt-and-braces. Outside a form, the default is type="button" (a regular click target, no submit behaviour).

You can also use <input type="submit" value="Sign up">, which works the same way but makes the label a value attribute instead of the visible content. Most modern code uses <button> because the label can include other elements (icons, formatting), and the styling story is cleaner.

If a button inside a form is not a submit button — say, a "show password" toggle next to a password field — give it type="button" explicitly. Otherwise clicking it submits the form, which is almost never what you want.

not-submit.html
<button type="button" onclick="togglePassword()">Show password</button>

A small but common bug: a single button with no explicit type inside a form is a submit button, so pressing Enter in any text field activates it. If your form has multiple buttons and the "primary" one should be the submit, mark all the others type="button".

Names, ids, and the data the server sees

There are two attributes on form controls that look similar and do very different things: id and name. They confuse beginners often enough to deserve their own beat.

id is for connecting the input to its <label> (and to CSS or scripts). It is unique within the page and is not sent to the server.

name is the key the server sees. When the form submits, the browser builds a list of name=value pairs from every named control. An input with no name is not sent.

name-vs-id.html
<form action="/login" method="post">
<label for="user-email">Email</label>
<input id="user-email" name="email" type="email" required>

<label for="user-pw">Password</label>
<input id="user-pw" name="password" type="password" required>

<button type="submit">Log in</button>
</form>

The server receives email=... and password=.... The ids user-email and user-pw exist only to wire labels to inputs in the page; they never leave the browser.

Two practical corollaries:

  • A control with no name is invisible to the server. Forget the name and your server endpoint sees an empty body. This is the single most common form bug.
  • Two controls with the same name send their values together. That is exactly how a group of type="checkbox" or type="radio" controls works — they share a name, and the server gets one value (radio) or many (checkbox) under that key.
check your understanding
You build a contact form, fill it out, and submit. The server logs an empty request body. The most likely cause is:
check your understanding
You have a "Save" submit button and a "Show password" toggle button inside the same form. The toggle button has no type attribute. The user types into the password field and presses Enter. What happens?
check your understanding
You want a "remember me" checkbox that, if checked, sends remember=on to the server. Which markup is correct?
← prevfinish course →
KeepLearningcertificate
for completing
HTML basics
0 of 8 read