Plate 1.6 · Phase 1 — Framing · 30 minutes
Forms: how users talk back
Everything so far has been one-way: you publish, people read. Forms are the moment the conversation reverses — a search box, a sign-up, a booking, a checkout. Every web app you'll ever build is, underneath, a set of forms.
Warm-up · from Module 1.5How many <main> elements should a page have — and what points at it?
Exactly one, holding the content unique to that page. The skip link at the top of the page points to it.
The paper form at the DMV
Think of a form at a Department of Motor Vehicles office. Each box has its label printed right beside it. Required fields are marked. The clerk at the window glances over it and hands it back if you left the date blank — and then, after you've gone, someone in the back office checks everything again properly before anything is issued.
At the DMV
- The printed label beside each box
- You always know what a box is for, even after you've written in it.
- The box itself
- Shaped for its content — tick boxes, a date grid, a signature line.
- The clerk at the window
- A quick check for obvious gaps, so you can fix them on the spot.
- The back office
- The real, final check. The clerk's glance is a courtesy, not security.
On the web
<label>- Stays visible and tells screen readers what each field is.
<input type="…">- The right type gives the right keyboard, picker and checks.
- Browser validation
required,type="email"and friends — instant feedback before sending.- Server validation
- The real check, in Phase 7. Anyone can bypass the browser's version in seconds.
The anatomy of a form
<form action="/inquiry" method="post">
<label for="name">Your name</label>
<input id="name" name="name" type="text"
autocomplete="name" required>
<label for="email">Email</label>
<input id="email" name="email" type="email"
autocomplete="email" required>
<button type="submit">Send inquiry</button>
</form>
| Piece | Job | Get it wrong and… |
|---|---|---|
| action | Where the data is sent. | It goes to the current page, which usually can't do anything with it. |
| method | get or post — how it's sent (below). | Passwords end up in the address bar and browser history. |
| label for="…" | Names the field. Matches the input's id. | Screen readers announce "edit text, blank". Clicking the label no longer focuses the box. |
| name | The key the value is sent under. | The field is silently left out of what's sent. The single most common form bug. |
| type | What kind of data — email, phone, date… | Wrong keyboard on phones, no built-in checking. |
| autocomplete | Lets the browser fill saved details. | People retype their address on a phone, and many give up. |
Notice that id and name do different jobs even though they often hold the same word: id connects the label on this page; name labels the data when it leaves.
Every field gets a label and a name.
The label is what people read and tap; the name is what arrives at the other end. Miss either and the form fails someone.
GET or POST: a postcard or an envelope
- GET writes the answers into the URL itself:
/search?q=hiking+boots&size=10. Like a postcard — anyone handling it can read it. Perfect for searches and filters, because the result can be bookmarked and shared. Never for anything private. - POST puts the answers inside the request, out of the address bar and browser history. Like an envelope. Use it for anything that creates, changes or is private: sign-ups, logins, orders, messages.
One honest caveat: an envelope keeps the answers out of sight, but it isn't a lock. What actually seals the envelope — so nobody along the way can read it — is HTTPS, from Module 0.1. Private data needs both: POST and HTTPS.
Fill it in and see what gets sent
This is a real form with real browser validation. Try submitting it empty, or with a bad email. When it's valid, you'll see exactly what would leave your browser — then flip the options to see GET versus POST, and what happens when a field forgets its name.
What would be sent
Nothing yet. Press Send inquiry — try it empty first.
The catering form has five fields: name, email, event_date, guests (default 25, between 10 and 200), and service, plus a notes box. Filled in with Ana Smith, wham@example.com, 2026-11-14 and "Oat milk please", here is what is sent.
| Method | What travels |
|---|---|
| POST | The data inside the request, to https://cedarandpine.com/inquiry. Nothing shows in the address bar or history. Right for anything private. |
| GET | Everything in the address: ?name=Ana+Smith&email=ana%40example.com&event_date=2026-11-14&.... Visible and saved in history. Fine for a search, wrong here. |
GET, with no name on the date field | The same address, but event_date is gone. No error appears anywhere. |
Spaces became + and special characters were encoded, the URL rules from Module 0.3. Notice that a field without a name attribute is silently left out.
Input types: the right box for the job
The type attribute changes the keyboard a phone shows, the picker a browser offers, and the checking it does for free. Pick one and try it — on a phone, the keyboard difference is dramatic.
| Type | Looks like | What it gives you |
|---|---|---|
email | Text box | Keyboard with @ and .com on phones. Rejects anything not shaped like an address. |
tel | Text box | Number pad on a phone. Does not validate, since formats vary. |
url | Text box | Keyboard with / and .com. Wants a full address, https:// included. |
number | Box with arrows | Number keyboard and min, max, step checks. Not for ZIP codes. |
date | Calendar picker | Always sent as YYYY-MM-DD, the sortable format from Module 0.3. |
password | Dots | Hides typing; password managers can fill it. |
search | Text box | Search key on phones, often a clear (x) mark. |
range | Bar with a handle | Rough preferences only. Hard to hit an exact value. |
color | Swatch | Native picker. Sends a hex value like #0d6e6b, met in Module 2.3. |
<label for="x">Email</label> <input id="x" name="x" type="email" autocomplete="email">
Notice that the markup is the same each time. Only the type changes.
Choices: checkboxes, radios and dropdowns
- Radio buttons — pick exactly one. Every radio in a group shares the same
name; that's what makes them a group. - Checkboxes — pick any number, including none.
<select>— pick one from a long list, like a US state. Under about six options, radios are friendlier because every choice is visible.
Wrap a group of radios or checkboxes in <fieldset> with a <legend>. The legend is the question — "Service", "Dietary needs" — and a screen reader announces it before each option, so "Drop-off" isn't heard with no idea what it's an answer to.
<label for="state">State</label> <select id="state" name="state" autocomplete="address-level1"> <option value="">Choose…</option> <option value="OR">Oregon</option> <option value="WA">Washington</option> </select>
Validation you get for free
| Attribute | Checks | Example |
|---|---|---|
| required | Can't be empty. | Name, email. |
| type="email" | Looks like an address. | Rejects jane@. |
| minlength / maxlength | Length of text. | A message of at least 20 characters. |
| min / max | Range for numbers and dates. | 10 to 200 guests; min="2026-10-01" on a date. ("No dates before today" needs JavaScript — Module 4.4.) |
| pattern | A custom format. | pattern="\d{5}" for a five-digit US ZIP code. |
Anyone can open DevTools, delete required, and submit whatever they like — you've done exactly that kind of editing since Module 0.1. Browser validation exists to help honest people fix mistakes quickly. The server must check everything again, every time. That's Phase 7, and it is non-negotiable.
Your contact form, in full
Here's the markup for the build below, with the three pieces you haven't seen written out yet: a <textarea> for a longer message, minlength, and a radio group. Each radio needs the same name and its own value — without a value, the form sends the useless word "on".
<label for="message">Message</label>
<textarea id="message" name="message" rows="5"
minlength="20" required></textarea>
<fieldset>
<legend>How did you hear about me?</legend>
<label>
<input type="radio" name="source"
value="friend"> A friend
</label>
<label>
<input type="radio" name="source"
value="linkedin"> LinkedIn
</label>
<label>
<input type="radio" name="source"
value="search"> A search engine
</label>
</fieldset>
Wrapping the radio inside its <label> is the other way to connect them — no for and id needed.
Where does the data go before you have a server?
Until Phase 7 you have no backend of your own. For a static site's contact form, form services fill the gap: Formspree gives you an address to put in action and emails you each submission; Netlify Forms does the same once your site is hosted on Netlify (Module 5.6), by adding one attribute to the form. Free for low volumes, and perfectly professional for a small business site.
Three things beginners get wrong
1. Placeholder instead of label. Gray hint text inside a box vanishes the moment someone types, is usually too faint to read comfortably, and isn't a reliable name for the field. Use a real <label>; a placeholder can add an example, never replace the label.
2. Forgetting name. The form looks perfect, submits happily, and that field never arrives. You just saw it happen above.
3. Asking for too much. Every extra field loses people. If you won't use their phone number, don't ask for it.
Words you now own
- form
- The container that gathers fields and sends them.
- action / method
- Where the data goes, and how — GET or POST.
- label
- The visible, announced name of a field.
- name
- The key a field's value is sent under.
- fieldset / legend
- A group of related fields and the question they answer.
- client-side validation
- Checking in the browser, for convenience.
- server-side validation
- Checking on the server, for real.
- autocomplete
- Hints that let the browser fill saved details.
Add a working contact form to your site.
What you should see — one way to do it
<main id="main">
<h1>Contact me</h1>
<form action="https://formspree.io/f/your-id" method="post">
<label for="name">Your name</label>
<input id="name" name="name" type="text"
autocomplete="name" required>
<label for="email">Email</label>
<input id="email" name="email" type="email"
autocomplete="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5"
minlength="20" required></textarea>
<fieldset>
<legend>How did you hear about me?</legend>
<label>
<input type="radio" name="source"
value="friend"> A friend
</label>
<label>
<input type="radio" name="source"
value="linkedin"> LinkedIn
</label>
</fieldset>
<button type="submit">Send message</button>
</form>
</main>
What you should see
- Submit it empty: the browser stops you with a bubble on the first empty required field.
- Type a 10-character message: a bubble says to use at least 20 characters.
- Click the word "Email": the cursor jumps into the email box. That's the label working.
- No Formspree yet? Leave
actionout for now. Submitting reloads the page with your answers in the address bar (?name=…) if you switch tomethod="get"— a handy way to see exactly what would be sent.
Six questions. Four correct to move on.
"Every field gets a label and a name. The right type gives the right keyboard. GET is a postcard, POST is an envelope, and HTTPS seals it. The browser's checks are a courtesy; the server's are the law." Your site can now listen, not just speak.