Forms and inputs
A form is the only part of a page where a visitor gives you something. It is also where most sites are worst — labels that are not labels, keyboards that show the wrong keys, error messages nobody can find, and an eight-field form for a phone number and an address.
HTML gives you more than most people use. This lesson is the part that does not need any JavaScript at all.
The smallest complete form
<form action="/enquiry" method="post">
<label for="name">Your name</label>
<input type="text" id="name" name="name" required>
<button type="submit">Send enquiry</button>
</form>
Five things in there matter.
action is where it goes. method="post" means the data goes in the
request body; get puts it in the URL, which is right for a search box and wrong
for anything private — a URL ends up in browser history, server logs and the
address bar.
name is what the server receives. A field with no name is not
submitted at all — it silently vanishes, and this is a genuinely common bug
because the field looks perfectly normal on the page.
id exists so the label can point at it, which is the next section.
<button type="submit"> submits. Get into the habit of writing type on every
button: a <button> inside a form defaults to submit, so a button you added for
something else will submit your form unexpectedly. type="button" for anything
that is not a submit.
Labels, properly
<!-- not a label. Just text that happens to sit nearby -->
<span>Your name</span>
<input type="text" name="name">
<!-- explicit: for matches id -->
<label for="name">Your name</label>
<input type="text" id="name" name="name">
<!-- implicit: wrapping, no id needed -->
<label>
Your name
<input type="text" name="name">
</label>
A real <label> does three things:
- A screen reader announces it when focus reaches the field. Without one, the user hears "edit text, blank" and has no idea what to type.
- Clicking the label focuses the field — and for a checkbox, toggles it. That turns a 16-pixel checkbox into a target the size of its text, which on a phone is the difference between usable and infuriating.
- It survives CSS changes and reflows, because the association is in the markup.
Either form is fine. The wrapping form cannot get out of sync; the for/id form
is easier to lay out. Pick one and be consistent.
placeholder is not a label. It disappears the moment somebody types, so a
person who is interrupted cannot see what the field was for. It also has poor
contrast by default, and many screen readers ignore it. Use it for an example of
the format, never as the only label:
<label for="pincode">Pincode</label>
<input type="text" id="pincode" name="pincode" placeholder="411038">
Input types, and the ones that matter on a phone
The type attribute changes the on-screen keyboard, which is the single
biggest usability difference on a mobile site.
<input type="text"> <!-- default -->
<input type="email"> <!-- keyboard with @ -->
<input type="tel"> <!-- big number pad, no letters -->
<input type="number"> <!-- numeric, with spinners -->
<input type="url">
<input type="password">
<input type="search">
<input type="date"> <!-- native date picker -->
<input type="time">
<input type="file">
<input type="checkbox">
<input type="radio">
<input type="range">
<input type="color">
<input type="hidden">
Two warnings worth having up front.
Do not use type="number" for a phone number, pincode or OTP. It looks right
and it is wrong, in three ways that are each easy to verify:
maxlengthis ignored. Type ten digits into<input type="number" maxlength="6">and you get ten. The same attribute on atype="text"field correctly stops at six. So you cannot limit a mobile number to ten digits with the obvious attribute.- It accepts things that are not phone numbers.
1e3and-5are both valid numbers, so both are accepted and both are submitted as typed. - The value can change by accident. A number field responds to the arrow keys
and to the scroll wheel while focused. A pincode of
411038becomes411039from one stray scroll, with no indication that anything happened.
(It does not strip leading zeros, which is often claimed — 007 submits as
007. What it does is treat the value as a number underneath, so
input.valueAsNumber reads 7, and anything that round-trips through that loses
the zeros.)
Use:
<input type="tel" inputmode="numeric" autocomplete="tel" maxlength="10">
type="number" is for quantities you would do arithmetic on.
inputmode controls the keyboard independently of the type, which is the tool
you want for a pincode:
<input type="text" inputmode="numeric" pattern="[1-9][0-9]{5}" maxlength="6"
autocomplete="postal-code">
autocomplete is not optional
<input type="text" id="name" name="name" autocomplete="name">
<input type="email" id="email" name="email" autocomplete="email">
<input type="tel" id="phone" name="phone" autocomplete="tel">
<input type="text" id="pincode" name="pincode" autocomplete="postal-code">
<textarea id="address" name="address" autocomplete="street-address"></textarea>
With the right autocomplete values, a returning visitor fills your whole form
with one tap. Without them, they type their address on a phone keyboard, and a
meaningful number of them give up instead.
autocomplete="off" on a normal field is almost always a mistake. The exception is
a genuinely one-time value, like an OTP — and for that there is
autocomplete="one-time-code", which lets the phone offer the code from the SMS.
Native validation, free
<input type="email" required>
<input type="tel" pattern="[6-9][0-9]{9}" required
title="A 10-digit Indian mobile number">
<input type="number" min="1" max="20" step="1">
<input type="text" minlength="3" maxlength="60">
The browser checks these before submitting and shows its own message. No JavaScript, works with the keyboard, translated into the user's language already.
Two things to know:
pattern needs a title, because the browser's default message for a failed
pattern is useless — it will say the format is wrong without saying what the format
is. The title becomes part of that message.
This is convenience, never security. Every one of these checks runs on the visitor's machine and can be removed in devtools in four seconds. The server must check everything again. That is the whole point of the client/server line from lesson one, and the Full-Stack course spends a module on it.
You can style based on validity, which is worth knowing now:
input:invalid { border-color: #b3261e; }
input:user-invalid { border-color: #b3261e; } /* only after they have interacted */
Use :user-invalid. Plain :invalid makes every required field red the instant
the page loads, which is hostile — you are telling somebody off before they have
typed anything.
Grouping: radios, checkboxes and fieldsets
<fieldset>
<legend>Delivery slot</legend>
<label><input type="radio" name="slot" value="morning" checked> Morning (8–11am)</label>
<label><input type="radio" name="slot" value="evening"> Evening (5–8pm)</label>
</fieldset>
Radios in a group must share the same name. That is what makes them mutually
exclusive — and a very common bug is giving each one a different name, after which
all of them can be selected at once and the server receives whichever it likes.
<fieldset> and <legend> are what tell a screen reader user that these three
radios are one question. Without them the user hears three unrelated options and
never hears "Delivery slot".
Checkboxes are for independent choices, and each needs its own name — or the same
name if you want an array of values on the server.
<select>, <textarea> and the rest
<label for="area">Area</label>
<select id="area" name="area" required>
<option value="">Choose an area</option>
<option value="kothrud">Kothrud</option>
<option value="karve-nagar">Karve Nagar</option>
</select>
<label for="notes">Delivery notes</label>
<textarea id="notes" name="notes" rows="4" maxlength="200"></textarea>
The empty first <option> matters: without it the first real option is
pre-selected, so required passes and you receive "Kothrud" from somebody who
never chose it.
<textarea> has no value attribute — its content sits between the tags — and
rows sets its initial height.
For fewer than about five options, radio buttons beat a <select>: everything is
visible, and there is no dropdown to open on a phone.
What a good form looks like overall
Short. Every field you remove increases the number of people who finish.
Ask yourself of each one: what happens if I do not have this? A small shop taking an enquiry needs a name, a phone number and a message. It does not need a title, a company name, a fax number or a date of birth.
One column, not two — a two-column form is read in the wrong order by a screen reader and by roughly half of sighted people too. Labels above fields, not beside them, because that survives a narrow screen without rearranging.
And test it with the keyboard only. Tab through your form, in order, and submit it with Enter. If you cannot, neither can a lot of your visitors.
Check your work
Why method="post". get puts the data in the URL, and URLs are logged and
kept in history.
What a missing name does. The field is not submitted, silently.
Why write type on every button. A <button> in a form defaults to submit.
Three things a real <label> does. Announces the field, makes the text a click
target, and survives layout changes.
Why placeholder is not a label. It disappears on typing, has poor contrast,
and is often ignored by screen readers.
Why not type="number" for a phone or pincode. maxlength is ignored, 1e3
and -5 are accepted, and the arrow keys or scroll wheel silently change the
value. Use type="tel" with inputmode.
What autocomplete is worth. A whole form filled with one tap.
Why pattern needs title. Otherwise the browser says the format is wrong
without saying what it is.
Why native validation is not security. It runs on the visitor's machine.
:invalid versus :user-invalid. The first turns everything red on load.
Why radios share a name. That is what makes them mutually exclusive.
What <fieldset>/<legend> do. Tell a screen reader the options are one
question.
Why an empty first <option>. Otherwise required passes with a value nobody
chose.
Practice
- Build an enquiry form with name, phone and message. Make every field labelled.
- Remove one
nameattribute, submit, and inspect what was sent in the Network tab. - Replace a
<label>with a<span>and listen to the field with a screen reader. Then put the label back. - Click a checkbox's label text and confirm it toggles. Then remove the label association and try again.
- Try
type="text",type="tel"andtype="number"for a phone number on a real phone. Photograph the three keyboards. - Put
maxlength="6"on atype="number"field and type ten digits. Then do the same on atype="text"field. Explain the difference. - Type
1e3into atype="number"field and submit it. Then focus a number field showing411038and scroll the wheel over it. - Add correct
autocompletevalues to every field, then fill the form on your phone with autofill. - Add
requiredandpatternwith notitle, submit, and read the message. Add atitleand read it again. - Style
input:invalid, load the page, and note how it feels. Change to:user-invalid. - Give two radio buttons different
namevalues and try to select both. - Wrap a radio group in
<fieldset>/<legend>and listen to it. Then remove them. - Build a
<select>without an empty first option and submit without choosing. - Tab through your whole form and submit with Enter, using no mouse at all.
- Take a real form you find annoying and list the fields you would delete.
Official documentation
- MDN — Web forms learning path — A complete course on forms in its own right, including styling and validation.
- MDN — The autocomplete attribute — Every valid token. Use this list rather than guessing at names.
- MDN — The input element — All input types and which attributes apply to each, which is how you check things like
maxlength. - W3C WAI — Forms tutorial — Labels, grouping and error messages from the accessibility side.
Next: video, audio, and putting somebody else's page inside yours.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship