Forms that work
Inputs, labels, validation, submission — the bits browsers do for free, and the bits you have to wire up.
A form is the second-oldest interactive element on the web (right after the link), and most projects re-implement parts of it that the browser already does. Built-in validation, automatic focus management on submit failure, a free POST request when JS is unavailable — all of it is sitting in the platform, and you only need to know which attributes to reach for.
The form element
<form> wraps a group of inputs that submit together. Two attributes set the contract:
action— the URL the form posts to. Relative or absolute.method—get(default) orpost.getputs the form data in the URL query string;postputs it in the request body. Usegetfor searches and filters (the URL becomes shareable); usepostfor anything that changes server state.
<form action="/search" method="get"> <input type="search" name="q" placeholder="Search docs"> <button type="submit">Search</button> </form>
When this form submits with q=html, the browser navigates to /search?q=html. No JavaScript anywhere — the form works on a flaky network, on a screen reader, on a feature phone.
That's the principle behind every other choice in this lesson: forms should work before you add JS, not stop working when JS fails.
Inputs and types
<input> is the workhorse. The type attribute decides what kind of input it is — and on mobile, what kind of keyboard the OS shows.
The types worth memorizing:
text— single-line plain text. The default.email— email address. Mobile shows the email keyboard (with@). Built-in validation rejects strings without an@.url— a URL. Mobile shows the URL keyboard.tel— phone number. Mobile shows the numeric phone keypad. No format validation (phone-number formats vary too much).number— numeric input with optionalmin,max,step. Up/down spinner.date,time,datetime-local— native date/time pickers.password— masked text. (Doesn't encrypt anything — TLS does that. Just hides the characters from over-the-shoulder viewing.)search— text input styled for search; some browsers show a clear-button.checkbox,radio— toggle and grouped-toggle.file— file upload.hidden— server-set values you want submitted but not displayed.submit,reset,button— buttons. Most projects use<button>instead; it's more flexible.
The single most useful detail: name is what becomes the form data key. An input without a name doesn't get submitted. Newcomers debug missing form data for hours before discovering this.
<form action="/signup" method="post"> <input type="email" name="email" required> <input type="password" name="password" required minlength="8"> <input type="checkbox" name="newsletter" value="yes"> <button type="submit">Sign up</button> </form>
Labels
A <label> describes what an input is for. Two ways to associate one:
<!-- explicit: the for attribute matches the input's id --> <label for="email">Email</label> <input type="email" id="email" name="email"> <!-- implicit: the input nests inside the label --> <label> Email <input type="email" name="email"> </label>
Both are correct. The explicit form is a touch more flexible (the label and input can be styled independently); the implicit form is a touch tighter to write. Pick one and use it consistently.
What you can't do is leave the label off. Three things break:
- The screen reader announces "edit text, blank" — the user has no idea what they're typing into.
- Clicking the label no longer focuses the input. Sounds minor, but on mobile the click target effectively shrinks from "the label and the input" to "just the input."
- Browser autofill struggles to identify the field.
placeholder is not a label. It disappears the moment the user types, so users with short-term memory issues lose the prompt; it sits at lower contrast than real text, so users with low vision struggle to read it; and it doesn't satisfy the screen-reader announcement. Use placeholder for examples ("e.g. ada@example.com") on top of a real label, never as a substitute.
Validation
The browser will validate inputs for free, before the form submits. Several attributes drive it:
required— the field must have a value.minlength,maxlength— character bounds for text inputs.min,max,step— numeric/date bounds for number, date, range.pattern="..."— a regex the value must match.type="email"/type="url"— built-in format validation for those types.
When the user submits a form with invalid fields, the browser shows a tooltip on the first invalid field, focuses it, and aborts the submission. No JavaScript involved.
<form action="/account" method="post">
<label>
Username
<input type="text" name="username" required
pattern="[a-z0-9_]{3,20}"
title="3–20 lowercase letters, digits, or underscores">
</label>
<label>
Age
<input type="number" name="age" min="13" max="120">
</label>
<button type="submit">Save</button>
</form>Built-in validation runs on the client. It's a UX layer, not a security boundary. The server still has to validate every field — assume an attacker bypasses the browser entirely (because they will).
For richer, custom messages, the constraint validation API in JavaScript (input.setCustomValidity("...")) lets you override the default tooltip text without abandoning the platform's submission flow.
required on every input. The user submits an empty form. What does the browser do?Submitting and method
A form submits when the user clicks a <button type="submit"> (or <input type="submit">), or presses Enter inside a single-line input. type="submit" is the default for a <button> inside a form — buttons that aren't meant to submit (an "open menu" button, say) need an explicit type="button" or they'll submit by accident.
The submission fires formdata and submit events on the form. JavaScript can intercept with event.preventDefault() and handle the submission via fetch — but the form should still have a working action attribute, so it functions if the script doesn't load.
A small set of attributes on the submit button override the form's defaults:
formaction— submit to a different URL than the form'saction. Useful for "Save" vs "Save and continue editing" buttons.formmethod— override the method.formnovalidate— skip validation for this submit. Useful for "Save draft" alongside a stricter "Publish."
<form action="/article" method="post"> <textarea name="body" required></textarea> <button type="submit">Publish</button> <button type="submit" formaction="/article/draft" formnovalidate> Save draft </button> </form>
This is the closing principle: most form behavior you'd reach for JavaScript to write — multi-action submit, validation, focus on first error, encoded form bodies — already exists. Knowing the platform-level forms model means writing less JavaScript that does the same thing slightly worse.