RizTech Academy logo
RizTech Academy
HTML FoundationsLesson 7 of 935 min

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:

  1. 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.
  2. 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.
  3. 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:

  • maxlength is ignored. Type ten digits into <input type="number" maxlength="6"> and you get ten. The same attribute on a type="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. 1e3 and -5 are 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 411038 becomes 411039 from 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

  1. Build an enquiry form with name, phone and message. Make every field labelled.
  2. Remove one name attribute, submit, and inspect what was sent in the Network tab.
  3. Replace a <label> with a <span> and listen to the field with a screen reader. Then put the label back.
  4. Click a checkbox's label text and confirm it toggles. Then remove the label association and try again.
  5. Try type="text", type="tel" and type="number" for a phone number on a real phone. Photograph the three keyboards.
  6. Put maxlength="6" on a type="number" field and type ten digits. Then do the same on a type="text" field. Explain the difference.
  7. Type 1e3 into a type="number" field and submit it. Then focus a number field showing 411038 and scroll the wheel over it.
  8. Add correct autocomplete values to every field, then fill the form on your phone with autofill.
  9. Add required and pattern with no title, submit, and read the message. Add a title and read it again.
  10. Style input:invalid, load the page, and note how it feels. Change to :user-invalid.
  11. Give two radio buttons different name values and try to select both.
  12. Wrap a radio group in <fieldset>/<legend> and listen to it. Then remove them.
  13. Build a <select> without an empty first option and submit without choosing.
  14. Tab through your whole form and submit with Enter, using no mouse at all.
  15. Take a real form you find annoying and list the fields you would delete.

Official documentation

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