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, or dialog (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 with id="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 same name; each has its own value, and only the chosen value is sent.
  • Checkboxes (type="checkbox"): each one is on or off. A ticked box sends its value (or on if 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) and new-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.

Form submission. The form's named fields become name=value pairs. With GET they are appended to the action URL as a query string; with POST they travel in the request body. Either way the server validates them and sends back a response. Form fields with a name reading=31245 &meter=E-5531 GET in the URL: ?reading=… POST in the request body Server
The same pairs, two ways to send them. The server validates them and answers with a new page or a redirect.

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.