Forms and Input
Collecting input from users: the form element, input types, labels, buttons, select and textarea, built-in validation, autocomplete, and what happens when a form is submitted.
The form element
The form element,
<form>, groups input controls and defines where
and how their values are sent when the user submits. Two attributes
do most of the work:
-
action: the URL that receives the data. Without it, the form is sent to the current page's URL. -
method:get(the default),post, ordialog(closes a surrounding<dialog>instead of sending anything).
Each control that should be sent needs a name. The name
is the key the server reads; the id is only for the
page itself (labels, links, CSS). A control without a
name is silently left out of the submission, which is a
classic cause of "the server never receives this field".
Here is the smallest useful version of the Kestrel Energy meter-reading form:
<form action="https://portal.example.com/meter-readings" method="post">
<p>
<label for="reading">Electricity meter reading (kWh)</label>
<input id="reading" name="reading" type="number" min="0" required>
</p>
<p>
<button type="submit">Save reading</button>
</p>
</form>
Labels
A form label is a
<label> element that names a form control. There
are two ways to connect them:
-
Explicit:
<label for="email">points to the control withid="email". The label and control can be anywhere on the page. -
Implicit: the control sits inside the label:
<label>Email <input name="email"></label>.
Both work; explicit labels give you more freedom in layout. The connection does three things. Clicking the label focuses the field or ticks the checkbox, which gives a much bigger target on a phone. The label becomes the control's accessible name, so a screen reader says "Email address, edit text" instead of just "edit text". And voice control users can say "click Email address". The WebAIM Million survey of 2026 found missing form labels on 51% of home pages, so this simple step is skipped remarkably often.
<p>
<label for="email">Email address</label>
<input id="email" name="email" type="email">
</p>
<p>
<label for="customer">Customer number</label>
<input id="customer" name="customer" inputmode="numeric"
aria-describedby="customer-hint">
<span id="customer-hint" class="hint">8 digits, on your annual statement</span>
</p>
<p>
<label><input name="newsletter" type="checkbox"> Send me outage alerts</label>
</p>
body {
font-family: system-ui, sans-serif;
}
label, .hint {
display: block;
}
.hint {
font-size: 0.875rem;
color: #595959;
}
The hint under the customer number is linked with
aria-describedby, so a screen reader reads it after the
label. Hints and format examples belong in visible text like this,
not in the placeholder.
Input types
The type attribute of <input>
decides what kind of control appears. An
input type is one of the values
HTML defines for that attribute, and each one gives the browser a
hint about the data: which keyboard to show on a phone, which picker
to offer, and which format to check. If the type is missing or
unknown, the input is a plain text field.
| Type | Use for | What the browser adds |
|---|---|---|
text |
Short free text: names, references | Nothing special (the default) |
email |
An email address | Keyboard with @; checks the basic format |
tel |
A phone number | Phone keypad on mobile; no format check (formats differ per country) |
url |
A web address | Keyboard with / and .; checks for an absolute URL |
password |
Passwords | Hides the characters; password managers recognize it |
number, range |
Quantities: a meter reading, a number of people |
Number keyboard,
min/max/step; a
slider for range
|
date, time,
datetime-local
|
Dates and times |
A date or time picker; the value is sent as e.g.
2026-10-01
|
search |
Search boxes | Often a clear button; "search" on the mobile Enter key |
checkbox, radio |
Yes/no choices, one choice from a few | See choices below |
file |
Uploads |
A file picker; needs method="post" and
enctype="multipart/form-data"
|
hidden |
Values the user does not see or edit | Nothing visible; still sent, and still editable by anyone with developer tools |
The full list in the HTML standard also has month,
week, color, submit,
reset, image and button.
Watch out with the first three: as of 2026, month and
week work in Chromium-based browsers and on some phones
(Safari on iOS, Firefox for Android), but not in desktop Firefox or
Safari, and color is not marked as fully supported in
all major browsers either. In browsers without support, they fall
back to a plain text field.
<p>
<label for="mail">Email</label>
<input id="mail" name="email" type="email">
</p>
<p>
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel">
</p>
<p>
<label for="move">Moving date</label>
<input id="move" name="move-date" type="date" min="2026-10-01">
</p>
<p>
<label for="people">People in the household</label>
<input id="people" name="people" type="number" min="1" max="12" value="2">
</p>
<p>
<label for="pw">Password</label>
<input id="pw" name="password" type="password">
</p>
body {
font-family: system-ui, sans-serif;
}
label {
display: block;
}
p {
margin: 0 0 0.75rem;
}
On a desktop the fields look much alike; the differences show on a phone (keyboards) and when you open the date field (a calendar). Click into the number field and use the arrow keys: the value stays between 1 and 12.
Checkboxes, radio buttons, select and textarea
For a fixed set of options HTML offers three controls:
-
Radio buttons (
type="radio"): pick exactly one. Buttons belong to one group when they share the samename; each has its ownvalue, and only the chosen value is sent. -
Checkboxes (
type="checkbox"): each one is on or off. A ticked box sends itsvalue(oronif it has none); an unticked box sends nothing at all. -
<select>with<option>elements: a drop-down list, useful for more options than fit comfortably as radio buttons.<optgroup label="…">groups the options.
For longer free text, <textarea> gives a
multi-line field. Unlike <input> it has an end
tag, and its initial text goes between the tags, not in a
value attribute.
Related controls belong in a <fieldset> with a
<legend>. For a group of radio buttons this is
essential: each button's label says "Fixed" or "Variable", and only
the legend says what the question was. Screen readers announce the
legend when the user enters the group.
<fieldset>
<legend>Tariff</legend>
<label><input type="radio" name="tariff" value="fixed" checked> Fixed, 1 year</label>
<label><input type="radio" name="tariff" value="variable"> Variable</label>
</fieldset>
<p>
<label for="meter-type">Meter type</label>
<select id="meter-type" name="meter-type">
<optgroup label="Electricity">
<option value="smart-e">Smart meter</option>
<option value="analog-e">Analog meter</option>
</optgroup>
<optgroup label="Gas">
<option value="smart-g">Smart gas meter</option>
</optgroup>
</select>
</p>
<p>
<label for="notes">Notes for the technician</label>
<textarea id="notes" name="notes" rows="3" maxlength="500"></textarea>
</p>
body {
font-family: system-ui, sans-serif;
}
fieldset label {
display: block;
padding: 0.15rem 0;
}
p label {
display: block;
}
textarea {
width: min(100%, 30rem);
}
Buttons
A submit button is a button
that submits its form: a
<button type="submit"> or an
<input type="submit">. Pressing Enter in a text
field also submits the form, through its first submit button. The
<button> element is the more flexible of the two,
because it can contain text, icons and other markup.
The type attribute of <button>
matters more than it looks:
type="submit": submits the form.-
type="button": does nothing by itself; used for buttons that run JavaScript, such as "Show password". -
type="reset": clears every field back to its initial value. Rarely what users want, and easy to press by accident.
A <button> without a type inside a
form acts as a submit button. The current HTML standard calls this
missing-value default the "Auto" state: it submits, unless the
button uses the newer command/commandfor
attributes or sits inside a <select>. In
practice: if a button should not submit, write
type="button".
<form action="https://portal.example.com/login" method="post">
<label for="login-pw">Password</label>
<input id="login-pw" name="password" type="password" autocomplete="current-password">
<!-- Without type="button", this would submit the form: -->
<button type="button" aria-controls="login-pw">Show password</button>
<button type="submit">Log in</button>
</form>
Give a button text that says what happens: "Save reading", "Log in", "Send request". "Submit" or "OK" tells the user little, and an icon-only button needs a text alternative (see accessible names). Avoid disabling the submit button until the form is valid: users cannot find out what is wrong with a button that does not respond. Let them press it and show what needs fixing.
Built-in validation
Constraint validation is the browser's built-in checking of form fields against rules written as attributes. When the user submits, the browser checks every field; if one fails, it cancels the submission, moves focus to the first invalid field and shows a message. The rules:
| Attribute | Rule |
|---|---|
required |
The field must not be empty (a checkbox must be ticked; one radio button of the group must be chosen) |
minlength, maxlength |
Number of characters typed |
min, max, step |
Range and steps for number, range, date and time fields |
pattern |
A regular expression the whole value must match |
type |
email and url check the format
|
The pseudo-classes :valid and :invalid let
CSS react to the result as the user types. Try the example: type a
postal code such as 2511 CV, then something like
25110. The house number is a text field, following the
tip above: real house numbers include 81-83 and
1A.
<form action="https://portal.example.com/move" method="post">
<p>
<label for="postcode">Postal code</label>
<input id="postcode" name="postcode" required
pattern="[1-9][0-9]{3} ?[A-Za-z]{2}"
aria-describedby="postcode-hint">
<span id="postcode-hint" class="hint">Four digits and two letters, like 2511 CV</span>
</p>
<p>
<label for="house">House number</label>
<input id="house" name="house" type="text" required maxlength="10"
pattern="[1-9][0-9]*( ?[A-Za-z]+)?(-[0-9A-Za-z]+)?">
</p>
<p>
<button type="submit">Check address</button>
</p>
</form>
body {
font-family: system-ui, sans-serif;
}
label, .hint {
display: block;
}
.hint {
font-size: 0.875rem;
color: #595959;
}
input {
border: 2px solid #595959;
padding: 0.25rem;
}
input:invalid {
border-color: #b91c1c;
border-style: dashed;
}
input:valid {
border-color: #0b6e4f;
}
Two things to notice. First, the empty required fields are "invalid"
from the moment the page loads, before the user has done anything,
so the red borders appear too early. The
:user-invalid pseudo-class only matches after the user
has interacted with the field (or tried to submit the form), and it
has been Baseline widely available since May 2026, so
input:user-invalid is usually the better choice.
Second, the invalid border is dashed as well as red: color alone is
not enough to show an error to someone who cannot tell red from
green (see
color and contrast). Real forms also show the error in text next to the field.
The house-number field has no inputmode="numeric": on
many phones a number keyboard has no letters, and
1A needs one. The postal-code pattern is for Dutch
postal codes; US ZIP codes, UK postcodes and other countries need
other patterns (see
postal codes). For international forms, validate the format per country on the
server, or not at all. UK addresses can have a building name and no
house number, which this required field does not accept.
Autocomplete
The autocomplete attribute tells the browser what a field is for, using standard tokens, so it can fill in saved values: a name, an address, an email address, a username and password. Without it, the browser has to guess from the field's name and label, and often guesses wrong.
<form action="https://portal.example.com/login" method="post">
<p>
<label for="user">Email or customer number</label>
<input id="user" name="username" autocomplete="username" required>
</p>
<p>
<label for="pass">Password</label>
<input id="pass" name="password" type="password"
autocomplete="current-password" required>
</p>
<p>
<button type="submit">Log in</button>
</p>
</form>
body {
font-family: system-ui, sans-serif;
}
label {
display: block;
}
Useful tokens, all defined in the HTML standard:
-
name,email,tel,street-address,postal-code: personal and address details. -
username,current-password(log in) andnew-password(sign up, change password): these let password managers fill in the right password, or offer to generate a new one. -
one-time-code: a code sent by text message, which phones can offer to fill in automatically.
Autocomplete is also an accessibility requirement: WCAG success criterion 1.3.5 (Identify Input Purpose, level AA) asks that fields about the user, such as name and address, identify their purpose. For people with memory or motor impairments, not having to type an address again is a real help.
What happens when a form is submitted
On submit, the browser walks through the form's controls, takes each
one that has a name, is not disabled and (for
checkboxes and radio buttons) is checked, and builds a list of
name=value pairs. By default it encodes that list the
same way as a URL query string, with
form encoding: pairs joined with &, spaces as +,
special characters percent-encoded. Then it sends an
HTTP request and
shows the response as a new page.
GET or POST
With method="get" the pairs are appended to the action
URL. A search form on the outage map might produce:
GET /outages?postcode=2511+CV&radius=5 HTTP/1.1
With method="post" the same encoding goes into the
request body, and the URL stays clean:
POST /meter-readings HTTP/1.1 Host: portal.example.com Content-Type: application/x-www-form-urlencoded reading=31245&meter=E-5531
The rule of thumb follows from where the data ends up:
- GET for searches and filters. The result can be bookmarked, shared and reloaded safely, because the request changes nothing. But the values end up in the address bar, browser history and server logs, so never use GET for passwords or personal data.
- POST for anything that changes data: saving a meter reading, logging in, placing an order. Reloading after a POST makes the browser ask whether to send the data again. Many sites therefore answer a successful POST with a redirect to a confirmation page.
For file uploads, add enctype="multipart/form-data" to
a POST form; the default encoding cannot carry files. A search form
can be wrapped in the <search> element, which
marks it as a search region for assistive technology.
Forms and security
Everything a form sends is input from outside, to be treated with suspicion: validate it on the server, escape it before showing it on a page (otherwise it may enable cross-site scripting), and protect forms that change data against cross-site request forgery. Forms with passwords or personal data must only be served and submitted over HTTPS. When JavaScript handles a form instead of the browser, it starts from the same submit event; see forms and default actions and sending form data in the JavaScript topic.