Forms.
How users talk to servers.
Every login. Every signup. Every search box. Every "contact us" message. Every checkout. All forms. HTML forms are how users send data to servers — the mechanism behind every interactive feature that isn't just clicking a link. This lesson teaches you the elements, the input types, the accessibility rules, and the validation that happens before your JavaScript even runs.
A form is a <form> containing <input> (or <textarea>, <select>) fields. Every field needs a <label> with for matching the input's id. Use the correct input type — type="email", type="password", type="number" — for mobile keyboards and free validation. Add required for mandatory fields. Use method="post" for anything sensitive or persistent. Submit with <button type="submit">.
Before this lesson: complete Lesson 3. You need labels, ids, and attributes covered in that lesson.
🏗️ Form structure
Five parts every form has. Miss any of them and something breaks.
<form action="/submit" method="post"> <label for="name">Name</label> <input type="text" id="name" name="name" required> <label for="email">Email</label> <input type="email" id="email" name="email" required> <label for="message">Message</label> <textarea id="message" name="message" rows="5"></textarea> <button type="submit">Send</button> </form>
action
The URL where the form data is sent. action="/submit" posts to /submit on the same server. Without action, the form submits to the current URL.
method
get (default) — appends data to the URL as query params. Visible, cacheable, appropriate for searches. post — sends in the request body. Not visible, appropriate for sensitive data.
name
The key the server receives the data under. name="email" means the server sees email=alice@example.com. Without name, the field's value is not sent.
id
Unique identifier within the page. Used to connect <label for="email"> to <input id="email">. Also useful for JavaScript (document.getElementById('email')).
Every field needs three attributes: id (unique, for the label), name (server key), and type (input type). Missing id breaks label association. Missing name means the server doesn't receive the field. Missing type defaults to text, but you lose the mobile keyboard and validation.
📝 Input types — a dozen you'll use
The right type gives you mobile keyboards and validation for free.
<input type="text" name="username"> <input type="email" name="email"> <input type="password" name="password"> <input type="number" name="age" min="0" max="120"> <input type="tel" name="phone"> <input type="url" name="website"> <input type="search" name="q"> <input type="date" name="birthday"> <input type="time" name="appointment"> <input type="color" name="favorite_color"> <input type="range" name="volume" min="0" max="100" value="50"> <input type="file" name="document" accept=".pdf,.doc"> <label> <input type="checkbox" name="subscribe" value="yes"> Subscribe to newsletter </label> <input type="radio" name="size" id="small" value="small"> <label for="small">Small</label> <input type="radio" name="size" id="large" value="large"> <label for="large">Large</label> <select name="country"> <option value="">Choose...</option> <option value="us">United States</option> <option value="uk">United Kingdom</option> </select>
type="email"
On mobile, shows a keyboard with @ visible. On submit, browser validates the format. Without this, users typing emails on phones get a plain keyboard — much slower.
type="checkbox"
Each checkbox is independent — multiple can be checked at once. The value attribute is what gets submitted when checked. If unchecked, nothing is submitted.
type="radio"
Radio buttons with the same name form a group — only one can be selected. Each needs a unique id and <label for>. This is the trick most beginners miss.
<textarea>
For multi-line text. Use rows and cols attributes for size. Content goes between tags — no value attribute. Default content: <textarea>Default text</textarea>.
Use the right type for the mobile experience alone. Even if you don't care about validation, using type="email" makes mobile users' keyboards show @ without switching. type="number" shows a numeric keypad. type="tel" shows the phone pad. That's the difference between a form that takes 30 seconds on mobile and one that takes 90. It's a tiny change with a huge UX impact.
🏷️ Labels — the accessibility rule you can't skip
Every input needs a label. Placeholders aren't enough.
<label for="email">Email address</label> <input type="email" id="email" name="email"> <label> Email address <input type="email" name="email"> </label> <input type="email" placeholder="Email address"> <input type="email" name="email"> <label>Email address</label> <input type="email" name="email"> <fieldset> <legend>Size</legend> <input type="radio" id="small" name="size" value="small"> <label for="small">Small</label> <input type="radio" id="large" name="size" value="large"> <label for="large">Large</label> </fieldset>
Why placeholders fail as labels:
1. They disappear. As soon as the user types, the hint is gone. If they forget what the field was for, they have to delete and re-read.
2. Poor contrast. Placeholders are styled light gray by default — often failing WCAG contrast requirements. Some users literally can't read them.
3. Screen reader issues. Support for placeholder announcements is inconsistent across browsers and screen readers. A real <label> is always announced reliably.
4. They break the click target. Clicking a <label> focuses the input. Clicking a placeholder doesn't — it's inside the input already.
<label for="id">
The standard pattern. for must match the input's id exactly. This creates a two-way link: screen readers announce the label, and clicking the label focuses the input.
Wrapped label
<label>Text <input></label> works too — the input is inside the label. No for/id needed. Common for checkboxes and radio buttons where the label goes next to the input.
fieldset + legend
For groups of related inputs (radio buttons, checkboxes), wrap them in <fieldset> with a <legend>. Screen readers announce the group name and how many options are inside.
Placeholders
Placeholders are hints, not labels. Use them for examples like "you@example.com", not for field names. Every field with a placeholder still needs a real <label>.
✅ Validation — free checks before JavaScript
HTML attributes give you browser-level validation with zero JS.
<input type="text" name="name" required>
<input type="email" name="email" required>
<input type="number" name="age" min="18" max="120">
<input type="text" name="username" minlength="3" maxlength="20">
<input type="text" name="zip" pattern="[0-9]{5}">
<input type="url" name="website">
<input type="date" name="start" min="2026-01-01" max="2026-12-31">
<input type="file" name="photo" accept="image/*">
<input
type="text"
name="username"
pattern="[a-zA-Z0-9]+"
title="Only letters and numbers"
>
<input type="email" name="email" autocomplete="email">
<input type="password" name="password" autocomplete="current-password">
<input type="text" name="name" autocomplete="name">
required
Marks a field as mandatory. The browser blocks submission and shows a message if empty. Works on input, textarea, and select.
type-based
type="email" validates email format. type="url" validates URL format. type="number" accepts only numbers. Zero extra code.
pattern
A regular expression the value must match. pattern="[0-9]{5}" for a 5-digit zip. title provides the error message. Pattern must match the entire value.
autocomplete
Tells browsers what kind of data goes in the field. autocomplete="email", "name", "current-password". Users can autofill in one click. Massive UX win.
Client-side validation is not enough. Anyone can open DevTools, remove required, and submit. Anyone can POST directly to your endpoint with curl. Always validate on the server — the browser check is for user convenience, not security. Treat client-side validation as "help users get it right the first time" and server-side validation as "the actual security boundary."
💻 Build something — a contact form
Every form skill in one working example.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Us</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 20px; }
form { display: flex; flex-direction: column; gap: 16px; }
label { font-weight: bold; }
input, textarea, select { padding: 8px; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 12px; font-size: 16px; background: #E34F26; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #F06529; }
</style>
</head>
<body>
<h1>Contact Us</h1>
<form action="/submit" method="post">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" required autocomplete="name">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required autocomplete="email">
<label for="phone">Phone (optional)</label>
<input type="tel" id="phone" name="phone" autocomplete="tel">
<label for="subject">Subject</label>
<select id="subject" name="subject" required>
<option value="">Choose a subject</option>
<option value="general">General inquiry</option>
<option value="support">Technical support</option>
<option value="billing">Billing question</option>
</select>
<fieldset>
<legend>Preferred contact method</legend>
<input type="radio" id="pref-email" name="preference" value="email" checked>
<label for="pref-email">Email</label>
<input type="radio" id="pref-phone" name="preference" value="phone">
<label for="pref-phone">Phone</label>
</fieldset>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required minlength="10"></textarea>
<label>
<input type="checkbox" name="subscribe" value="yes">
Send me news and updates
</label>
<button type="submit">Send Message</button>
</form>
</body>
</html>
This is production quality. Every field has a label with proper for/id. Every field has the right type. Radio buttons are grouped in a fieldset with a legend. autocomplete hints are set. required and minlength validate for free. This is exactly what a real contact form looks like — copy it and change the fields for any form you need.
🏆 Challenge
Three tasks. Each exercises a different form pattern.
Task 1 — Login form
Build a login form with these fields:
1. Email (required, type=email, autocomplete=username)
2. Password (required, type=password, autocomplete=current-password, minlength=8)
3. "Remember me" checkbox
4. Submit button saying "Log In"
Every field needs a proper label. The form should use method="post". Test that submitting with a short password triggers the browser's validation message.
Task 2 — Search form
Build a search form with:
1. A search input (type="search", required)
2. A submit button
3. Use method="get" — search queries should be visible in the URL
4. Test that submitting adds ?q=your+query to the URL
This is exactly how every search box works. The URL is shareable and bookmarkable.
Task 3 — Signup form with validation
Build a registration form with:
1. Username — required, minlength=3, maxlength=20, pattern for letters/numbers only
2. Email — required, type=email
3. Password — required, minlength=8
4. Confirm password — required, minlength=8 (note: HTML can't check they match — that needs JavaScript)
5. Age — required, type=number, min=13, max=120
6. Terms checkbox — required
7. Submit button
Test each validation: empty username, short username, invalid email, short password. The browser should block submission with a message for each.
⚠️ Common mistakes
The form bugs that catch every beginner.
Placeholders aren't labels — they disappear on typing, have poor contrast, and aren't reliably announced by screen readers. Every field needs a real <label>.
Without name, the field's value isn't submitted to the server. The form submits but that field is missing from the data. Always include name.
<label>Email</label><input id="email"> looks connected but isn't. The label needs for="email" matching the input's id. Otherwise clicking the label doesn't focus the input.
Radio buttons with different name attributes are independent — multiple can be selected at once. Fix: same name for the whole group, unique id for each.
Passwords in GET requests appear in the URL, browser history, and server logs. Fix: use method="post" for anything sensitive. GET is only for searches and filters.
Anyone can bypass HTML validation. Server-side validation is the real security boundary. Client-side is for user convenience; server-side is mandatory.
Without autocomplete="email", users can't autofill in one click. This is a big UX loss. Add autocomplete to every field: name, email, phone, address, password, credit card.
type="text" for email, phone, password, dates — all lose their mobile keyboards and validation. Fix: use the specific type. It's one word different with a massive UX difference.
➡️ What's next
You can build every element. Next: making it work for everyone.
❓ Frequently asked questions
What is an HTML form?
An HTML form is a container for input fields, buttons, and other controls that collect data from users. When submitted, the browser sends the data to a server. Forms are how users log in, sign up, search, send messages, and complete any action that requires typing.
What is the difference between input and textarea?
input is a single-line text field (or specialized types like email, password, number). textarea is a multi-line text field for longer content. Use input for short values, textarea for long text. textarea doesn't use a value attribute — content goes between tags.
Why do I need labels on form fields?
Labels tell users what each field is for — and screen readers announce them, so blind users know what to type. A placeholder isn't enough: it disappears on typing, has poor contrast, and isn't announced reliably. Every input needs a label with a for attribute matching the input's id.
What are the most common input types?
text (default), email, password, number, tel, url, checkbox, radio, submit, and hidden. Modern HTML5 added date, time, color, range, search, and file. Each type triggers a different keyboard on mobile and enables browser-level validation.
How does HTML form validation work?
Add required to make a field mandatory. Use type=email for email validation, pattern for custom regex, minlength/maxlength for length. The browser validates on submit. Always also validate on the server — client-side validation can be bypassed.
What is the difference between GET and POST?
GET appends form data to the URL as query parameters — visible in the address bar, cacheable, appropriate for search forms. POST sends data in the request body — not visible in the URL, appropriate for sensitive data. Use POST for login, signup, checkout.
How do I group radio buttons?
Radio buttons with the same name attribute are grouped — only one can be selected at a time. Give each radio a unique id and matching label for attribute. Wrap them in a fieldset with a legend to describe the group.
What is the difference between button and input type=submit?
button type=submit is the modern, flexible choice — can contain HTML content (icons, spans). input type=submit only accepts a value attribute for text. Both submit the form. Use button for anything more complex than plain text.