../

Forms & inputs

The markup side of forms: the <form> element and how submission works, every input type and attribute, labels and groups, buttons, select, textarea and friends, autofill tokens, mobile keyboards, built-in validation, file uploads and the accessibility rules. Reading values, FormData, the constraint validation API and fetch submission are in Forms (TypeScript); styling controls is in CSS; form layout and copy advice is in UX & UI.

The form element

<form action="/signup" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email"
    autocomplete="email" required>
  <button>Create account</button>
</form>
AttributeValuesNotes
actionURLwhere to send; defaults to the current URL
methodget (default), post, dialogget appends the data to the URL as a query string: use for searches and filters (bookmarkable, cacheable, back-button friendly). post sends it in the body: use for anything that changes state, and for passwords. dialog closes the enclosing <dialog> without submitting and sets its returnValue to the submit button's value
enctypeapplication/x-www-form-urlencoded (default), multipart/form-data, text/plainmultipart is required for file uploads; text/plain is for debugging only; ignored with get
novalidatebooleanskip built-in validation on submit (the API and :invalid still work)
autocompleteon (default), offdefault for every control; per-field tokens are better (see Autofill)
target_self, _blank, a frame namewhere the response loads
namestringdocument.forms.name access
relnoopener, noreferrer…like links, for target="_blank" submissions
accept-charsetUTF-8the only valid value; leave it out

Controls outside the form join it with form="form-id", and a submit button can override the form's settings for its own click with formaction, formmethod, formenctype, formnovalidate and formtarget (for example a "Save draft" button that skips validation).

Implicit submission

Pressing Enter in a text-like field submits the form by "clicking" its default button: the first submit button in tree order.

SituationWhat Enter does
Form has a submit buttonactivates it (fires its click, sends its name=value)
Default button is disablednothing
No submit button, one text-like fieldsubmits the form
No submit button, two or more text-like fieldsnothing
Focus in a textareainserts a newline

Text-like here means text, search, url, tel, email, password, number and the date and time types. So a form with only an icon-less "Next" <div> can't be submitted from the keyboard: always include a real submit button, even if it's visually hidden.

What gets submitted

The browser builds a list of name/value pairs from the form's controls, in tree order:

ControlSent asNot sent when
Text-like input, textareaname=value (even when empty)no name; disabled
Checkboxname=value (value defaults to "on")unchecked: nothing at all, not false
Radio groupname=value of the checked onenone checked
selectname=value for each selected optionno option selected
select multiple, several checkboxes with one namethe pair repeated: tag=a&tag=b
Submit buttonits name=value, only if it was the one pressedany other button
input type="image"name.x=12&name.y=7 (click coordinates)not the submitter
File inputthe file (multipart), or only the file name (urlencoded)no name
input type="hidden"name=value; name="_charset_" is filled with the encoding
dirname="comment.dir"an extra pair with the text direction, ltr or rtl
output, fieldset, button type="button"never
Anything disabled (or in a disabled fieldset)never
<form method="get" action="/search">   →   GET /search?q=red+shoes&size=9&size=10&sort=price
 
POST, application/x-www-form-urlencoded:
  email=sam%40acme.example&plan=pro&tags=ci&tags=perf
  (UTF-8, percent-encoded; spaces become +; newlines become %0D%0A)
 
POST, multipart/form-data:
  ------boundary
  Content-Disposition: form-data; name="title"
 
  Holiday
  ------boundary
  Content-Disposition: form-data; name="photo"; filename="beach.jpg"
  Content-Type: image/jpeg
 
  (binary bytes)
  ------boundary--

Input types

typeValue formatMobile keyboardBuilt-in validationBrowser UI and notes
textany string (newlines stripped)standardminlength, maxlength, patternthe default for an unknown type
searchany stringstandard, Enter labeled "Search"/"Go"as textsome browsers add a clear button and clear on Esc; role searchbox
emaila@b.co; comma list with multipleadds @ and .typeMismatch unless it looks like an address (a@b passes: no TLD needed)leading and trailing spaces trimmed
urlabsolute URLadds / and .comtypeMismatch unless absolute: example.com fails, https://example.com passestell users to include https://, or use text and normalize on the server
telany stringphone keypadnone (formats vary worldwide); add pattern if you mustautofill fills it with autocomplete="tel"
passwordany string (newlines stripped)standard, maskedas textpassword managers key on it; add a show-password toggle
numberdecimal string; "" if unparsablenumeric (varies by platform)min, max, step (default 1), badInputspinner; the mouse wheel (while focused) and arrow keys change the value; big values turn into exponents. Only for quantities: not card numbers, postcodes, phone numbers or IDs
rangenumber; never emptynonealways valid: out-of-range values are clampedslider; default range 0–100, default value the midpoint; imprecise, pair it with an output
date2026-09-26native date pickermin, max, step (days)shown in the user's locale format, value always ISO
time14:30 or 14:30:15.250time pickermin, max, step (seconds, default 60)12- or 24-hour display follows the locale
datetime-local2026-09-26T14:30date and time pickermin, max, stepno time zone: send the zone separately if it matters
month2026-09month pickermin, maxno desktop Firefox or Safari support: falls back to a plain text box
week2026-W39week pickermin, maxsame gaps as month
color#rrggbb, lowercasecolor pickeralways validdefault #000000; alpha and colorspace attributes are Safari-only
checkboxits value (default "on") when checkedrequired = must be checkedindeterminate only settable from script; the switch attribute (toggle look) is Safari 17.4+ only
radiovalue of the checked onerequired on any radio makes the group requiredsame name = one group; arrow keys move and select within it
filea file listfile, camera or photo pickerrequiredneeds multipart/form-data; accept, multiple, capture, webkitdirectory (Baseline 2025)
hiddenany stringnever validatednot secret: users can read and change it
submit, reset, button, imagevalue is the label (image uses alt and src)prefer <button>: it can contain markup

The date and time types are Baseline widely available; month and week are not. Where a type isn't supported the browser falls back to type="text", so server-side validation is required anyway.

input-types.html
<style>
  form { display: grid; gap: 6px 12px;
    grid-template-columns: max-content 1fr; }
  input { font: inherit; justify-self: start; }
</style>
<form>
  <label for="d">date</label>
  <input id="d" type="date" value="2026-09-26">
  <label for="t">time</label>
  <input id="t" type="time" value="14:30">
  <label for="dl">datetime-local</label>
  <input id="dl" type="datetime-local"
    value="2026-09-26T14:30">
  <label for="c">color</label>
  <input id="c" type="color" value="#1f6feb">
  <label for="r">range</label>
  <input id="r" type="range" min="0" max="10" value="7">
  <label for="n">number</label>
  <input id="n" type="number" min="0" max="9" value="3">
  <label for="s">search</label>
  <input id="s" type="search" value="red shoes">
</form>
Result

Input attributes

AttributeApplies toNotes
nameall controlsthe key in the submitted pairs; no name, not submitted
valuemostthe initial value; the live value is a property (reset restores the attribute)
requiredtext-like, select, textarea, checkbox, radio, file, date typesempty → valueMissing; also sets :required
min, maxnumber, range, date and time typesrangeUnderflow / rangeOverflow; for dates use the value format (min="2026-01-01")
stepsamevalid values are min + n × step; step="any" allows any decimal; step="0.01" for money
minlength, maxlengthtext-like, textareacounted in UTF-16 code units (an emoji can count 2); maxlength stops typing and truncates pastes; minlength doesn't apply to an empty field
patterntext, search, url, tel, email, passworda regular expression; see below
placeholdertext-like, textareaexample format only; disappears on typing and is not a label
readonlytext-like, date types, textareafocusable, selectable, submitted, not validated; not valid on checkboxes, radios, select
disabledevery control, fieldset, optgroup, optionnot focusable, grayed out, not submitted, not validated
multipleemail, file, selectseveral values
acceptfileimage/*, .pdf, image/png,image/jpeg; a hint for the picker, not a security check
capturefileuser (front camera) or environment (back); mobile only, ignored on desktop
listtext-like, range, color, date typesid of a <datalist> of suggestions
formevery controlid of the form it belongs to, if it sits outside it
formaction, formmethod, formenctype, formnovalidate, formtargetsubmit buttonsoverride the form's attributes for that button
autocompletetext-like, select, textareaautofill token; see below
autofocusanyone per page; skip on pages with content above the form
sizetext-likevisible width in characters; CSS inline-size is usually better
dirnametext-like, textareaalso submits the text direction under this name
checkedcheckbox, radioinitial state
inputmode, enterkeyhinttext-like, contenteditablevirtual keyboard layout and Enter-key label
spellcheck, autocapitalize, autocorrecttext-likeswitch off for usernames, emails, codes

pattern

<input name="postcode" pattern="[A-Za-z0-9 ]{5,8}"
  title="5 to 8 letters, digits or spaces">
<input name="slug" pattern="[a-z0-9\-]+">
  • Implicitly anchored: the browser wraps it as ^(?:pattern)$, so it must match the whole value. Don't add ^ and $.
  • Compiled with the v flag since mid-2023 (MDN; previously u). In v mode, -, (, ), [, ], {, }, / and | must be escaped inside a character class: [\w-] is now a syntax error. An invalid pattern is silently ignored (a console warning only), so every value passes.
  • Empty values pass pattern; add required to forbid them.
  • Some browsers show the title in the validation bubble. Put the rule in visible hint text too.

readonly vs disabled

readonlydisabled
Focusable, in tab orderyesno
Selectable and copyableyesoften not
Submittedyesno
Validatednono
Announced"read only""dimmed"/"unavailable"
Use forvalues the user needs to see and copy (an order ID)options that don't apply right now; explain why nearby

Labels, fieldsets & groups

<!-- explicit: for + id (works anywhere in the document) -->
<label for="name">Full name</label>
<input id="name" name="name" autocomplete="name">
 
<!-- implicit: wrap the control -->
<label>
  <input type="checkbox" name="terms" required>
  I accept the <a href="/terms/">terms</a>
</label>
RuleDetail
Every control needs a labelit gives the accessible name, and clicking it focuses or toggles the control (a bigger target)
Explicit or implicitboth are valid; explicit (for) is the most robust and lets the label sit anywhere; you can combine them
Multiple labels are allowedinput.labels lists them; all are read, which can be verbose
Label contentphrasing content; no other controls or nested labels; a link inside works but steals the click
Hints and errorsnot in the label: separate elements referenced by aria-describedby
Name priorityaria-labelledby > aria-label > label > title > placeholder (a last resort, not a label)
Keep visible text in the nameWCAG 2.5.3: voice users say what they see ("click Email")

fieldset + legend group related controls: the legend is announced as the group's name when a screen-reader user enters it. Use them for radio groups, checkbox groups, and composite fields such as an address or a date of birth.

  • legend must be the first child of fieldset.
  • <fieldset disabled> disables every control inside except those in its legend.
  • Keep legends short (they may be repeated for every field in some screen readers); put long instructions in a paragraph referenced with aria-describedby on the fieldset.
radio-group.html
<style>
  fieldset { border: 1px solid var(--chip);
    border-radius: 6px; padding: 8px 12px; }
  label { display: block; padding: 3px 0; }
  .hint { color: var(--muted); margin: 0 0 4px; }
</style>
<fieldset aria-describedby="plan-hint">
  <legend>Plan</legend>
  <p id="plan-hint" class="hint">Change it any time.</p>
  <label><input type="radio" name="plan" value="free"
    checked> Free</label>
  <label><input type="radio" name="plan" value="pro">
    Pro, £8 a month</label>
  <label><input type="radio" name="plan" value="team"
    disabled> Team (coming soon)</label>
</fieldset>
Result

Tab moves into the group (to the checked radio) and the arrow keys move between options. A group of checkboxes, by contrast, is one tab stop per box.

Buttons

MarkupDoesNotes
<button> in a formsubmitsthe default type is submit: every "Show password", "Add row" or icon button in a form must be type="button"
<button type="submit">submitssay it explicitly; name/value are sent for the pressed one
<button type="button">nothing by defaultfor script actions
<button type="reset">restores initial valuesalmost never wanted; users hit it by mistake
<button commandfor="dlg" command="show-modal">opens a dialog or popover declarativelywith commandfor/command a typeless button no longer submits; Baseline 2025
<input type="submit" value="Send">submitstext-only label; fine but less flexible
  • Label buttons with the action: "Create account", "Save changes", not "Submit" or "OK".
  • A button that only shows an icon needs a text alternative: visually hidden text or aria-label.
  • Don't disable the submit button until the form is valid; users can't discover what's missing. Disable it (or ignore repeat clicks) only while a submission is in flight.

select, datalist & textarea

select

<label for="country">Country</label>
<select id="country" name="country" required
  autocomplete="country">
  <option value="">Choose a country…</option>
  <optgroup label="Europe">
    <option value="GB">United Kingdom</option>
    <option value="IE">Ireland</option>
  </optgroup>
  <hr>
  <option value="other">Other</option>
</select>
FeatureNotes
option valuesubmitted value; defaults to the option's text
Placeholder optionvalue="" + required makes "Choose…" fail validation
optgroup labelnon-selectable group heading; disabled disables the whole group
hr inside selecta separator line in every current engine except Chrome on Android, which ignores it
multipleCtrl/Cmd-click to select several: poor discoverability; prefer checkboxes
size="5"shows a list box instead of a drop-down; iOS ignores it
Long liststyping a letter jumps to matching options, which keeps a country list usable; for hundreds of options or fuzzy matching, a searchable combobox is better

Customizable select. appearance: base-select on the select and on ::picker(select) opts into a fully stylable control: options can hold rich markup (icons, descriptions), a <button> child replaces the trigger, <selectedcontent> mirrors the chosen option, and the picker is positioned in the top layer with anchor positioning. As of September 2026 it ships in Chrome and Edge 135+ and Safari 27 (released September 2026); Firefox has it only behind a flag, so it is not Baseline. It is a progressive enhancement: browsers without it render a normal native select, so it is safe to use today.

select, ::picker(select) {
  appearance: base-select;
}
select::picker-icon { transition: rotate 0.2s; }
select:open::picker-icon { rotate: 180deg; }
option:checked { font-weight: 600; }

datalist

Suggestions, not constraints: the user can still type anything.

<label for="browser">Browser</label>
<input id="browser" name="browser" list="browsers">
<datalist id="browsers">
  <option value="Chrome"></option>
  <option value="Firefox"></option>
  <option value="Safari"></option>
</datalist>

Support is patchy for non-text types (no Firefox for date, time or color), the popup can't be styled, and it is only partly supported in Firefox for Android. Fine for low-stakes hints; use a real combobox when choosing from the list matters.

textarea

<label for="msg">Message</label>
<textarea id="msg" name="msg" rows="4" maxlength="2000"
  aria-describedby="msg-hint"></textarea>
<p id="msg-hint">Up to 2,000 characters.</p>
FeatureNotes
Initial valuethe element's content, not a value attribute; a newline straight after <textarea> is dropped
rows, colsinitial size in lines and characters
wrapsoft (default: visual wrapping only), hard (inserts newlines at the wrap points on submit; needs cols)
Line breakssubmitted as CRLF (%0D%0A)
field-sizing: contentCSS that makes it grow with its content; Baseline 2026 (June 2026). Add min-block-size and max-block-size
resizeCSS: vertical is usually right; none removes the handle
field-sizing.html
<style>
  textarea { font: inherit; inline-size: 100%;
    field-sizing: content; min-block-size: 2lh;
    max-block-size: 6lh; resize: vertical; }
</style>
<label for="note">Note (grows as you type, up to 6
  lines, where supported)</label>
<textarea id="note">Type a few more lines here…</textarea>
Result

output, progress & meter

ElementMeansAttributesRole
outputthe result of a calculation or user actionfor="a b" (ids of the inputs), name; not submittedstatus (a polite live region in most screen readers)
progresscompletion of a taskvalue, max (default 1); no value = indeterminate (spinner-like)progressbar
metera scalar measurement in a known rangevalue, min, max, low, high, optimummeter

Use progress for "uploading 40 %" and meter for "disk 90 % full" or a score. The browser colors a meter by which region value falls in relative to optimum. Both need a label and the value in text: their visual state alone isn't reliable for everyone.

progress-meter.html
<style>
  p { display: flex; align-items: center; gap: 8px;
    margin: 6px 0; }
  label { inline-size: 9ch; }
</style>
<p><label for="up">Upload</label>
  <progress id="up" value="40" max="100"></progress> 40 %</p>
<p><label for="wait">Loading</label>
  <progress id="wait"></progress> indeterminate</p>
<p><label for="disk">Disk</label>
  <meter id="disk" value="0.9" low="0.6" high="0.8"
    optimum="0.2"></meter> 90 % full</p>
Result

Autocomplete tokens

The autocomplete attribute tells browsers and password managers exactly what a field holds. It makes autofill fill the right fields, lets password managers save and suggest correctly, lets iOS offer SMS codes, and satisfies WCAG 1.3.5 Identify Input Purpose (AA). Browsers largely ignore autocomplete="off" on login fields, so fighting autofill fails; label fields properly instead.

GroupTokens
Namename, honorific-prefix, given-name, additional-name, family-name, honorific-suffix, nickname
Accountusername, current-password (sign-in), new-password (sign-up, change: managers suggest a strong one), one-time-code (SMS/app codes)
Contactemail, tel, tel-country-code, tel-national, tel-extension, impp
Addressstreet-address (multi-line, for a textarea), address-line1–address-line3, address-level2 (town or city), address-level1 (county, state, province), postal-code, country (ISO code), country-name
Cardcc-name, cc-number, cc-exp, cc-exp-month, cc-exp-year, cc-csc, cc-type
Otherorganization, organization-title, bday, bday-day, bday-month, bday-year, sex, language, url, photo, transaction-amount, transaction-currency
Prefixesshipping / billing before an address or contact token; section-name first, to keep two groups apart (section-guest2 email)
Suffixwebauthn last, to offer passkeys in the username field's autofill (username webauthn)
Switcheson, off
<input name="ship-zip"
  autocomplete="shipping postal-code">
<input name="user"
  autocomplete="username webauthn">

Keyboards: inputmode & enterkeyhint

inputmode changes the on-screen keyboard without changing validation or value handling, which makes type="text" inputmode="numeric" the right choice for codes, card numbers and postcodes.

inputmodeKeyboardUse for
textstandarddefault
numericdigits 0–9PINs, OTPs, card numbers, account numbers
decimaldigits and the locale's decimal separatorprices, measurements
telphone keypad with * and #phone numbers (or just type="tel")
emailwith @ and .emails in a text field
urlwith / and .URLs in a text field
searchsearch-optimized; Enter becomes "Search"search boxes
noneno virtual keyboardonly when the page provides its own keyboard

enterkeyhint sets the Enter key's label: enter, done, go, next, previous, search, send. It changes only the label, not the behavior; use next on fields that are not last and send on a chat box. Both attributes are Baseline widely available.

Built-in validation

On submit (unless the form has novalidate or the button formnovalidate), the browser checks each control's constraints, cancels the submission if any fail, focuses the first invalid one and shows a message bubble. The bubble can't be styled, its wording varies by browser and language, it disappears after a few seconds, and only one error shows at a time.

Pseudo-classMatchesUse
:invalid / :validconstraint state right now, from page loadnot for error styling: empty required fields are red before the user starts
:user-invalid / :user-validonly after the user has changed and left the field, or tried to submiterror styling; Baseline widely available (since May 2026)
:required / :optionalpresence of required"(optional)" labels
:in-range / :out-of-rangemin/max checks
:placeholder-shownempty field showing a placeholder

Type an invalid value, then leave the field. The left column uses :invalid (red from the start), the right :user-invalid:

user-invalid.html
<style>
  .cols { display: flex; gap: 16px; flex-wrap: wrap; }
  label { display: grid; gap: 4px; }
  input { font: inherit; border: 2px solid var(--chip);
    border-radius: 4px; padding: 4px; }
  .now input:invalid,
  .user input:user-invalid {
    border-color: var(--graph-1); }
</style>
<div class="cols">
  <label class="now">Email (:invalid)
    <input type="email" required></label>
  <label class="user">Email (:user-invalid)
    <input type="email" required></label>
</div>
Result

Two workable strategies:

  1. Native: keep browser validation, style with :user-invalid, set title on pattern fields, write hints next to the fields. No JavaScript, rough UX.
  2. Custom: novalidate on the form, then use the constraint API (checkValidity(), validity, setCustomValidity()) to show your own inline messages and an error summary. The markup stays the same; the script is in Forms (TypeScript).

Either way, validate again on the server: attributes are trivially bypassed.

File uploads

<form action="/upload" method="post"
  enctype="multipart/form-data">
  <label for="photos">Photos (JPEG or PNG, max 10 MB)</label>
  <input id="photos" name="photos" type="file"
    accept="image/jpeg,image/png" multiple required>
  <button>Upload</button>
</form>
RuleDetail
method="post" + enctype="multipart/form-data"otherwise only the file name is sent
acceptfilters the picker; users can still pick "All files"
capture="environment"opens the rear camera directly on mobile; omit it to let users choose camera or library
webkitdirectorypick a whole folder (Baseline 2025)
Limitsstate the size and type limits in the label or hint; enforce them on the server (and in the proxy: nginx rejects bodies over 1 MB by default)
valuereads C:\fakepath\name.jpg; can only be set to ""

Reading, previewing and uploading files with progress from script is in Forms (TypeScript).

Accessibility & UX rules

RuleWhy
Visible label on every fieldplaceholders vanish, fail contrast and aren't reliable names (WCAG 3.3.2)
Hints and errors linked with aria-describedbyread after the label; list several ids: aria-describedby="pw-hint pw-error"
Error text says what's wrong and how to fix it"Enter a date in the past", not "Invalid" (WCAG 3.3.1, 3.3.3)
aria-invalid="true" only after validationset it with the error, clear it with the fix
Error summary at the top on submit, linked to each fieldlong forms: users see everything at once (GOV.UK pattern)
Don't block pastepassword managers and people with memory or motor impairments rely on it; WCAG 3.3.8 treats copy-paste as an acceptable aid for authentication
Don't split fields needlesslyone field for a phone number, card number or OTP; splitting breaks paste, autofill and screen readers. Exception: day/month/year for memorable dates (GOV.UK's date input)
Don't ask twiceno "confirm email"; offer show-password instead of "confirm password"; don't re-ask for data already given (WCAG 3.3.7)
Mark optional or requiredwhichever is rarer; use words, not only *
One columneyes and focus move down a single line
16 px or larger text in inputsiOS Safari zooms into inputs with smaller text on focus
Don't auto-advance focussurprises users and breaks corrections
Targets at least 24×24 CSS pxWCAG 2.5.8; 44×44 is the comfortable touch target
Keep the submit button enableddisable only while submitting

Common mistakes

MistakeEffectFix
Button inside a form without type"Show password" or "Add" submits the formtype="button"
Control without namesilently missing from the submissionadd name
Placeholder as the only labelno name for screen readers; hint gone once typing startsa label
type="number" for card numbers, postcodes, phone numbersleading zeros lost, spinners, wheel changes the value, exponent notationtype="text" inputmode="numeric" (GOV.UK, 2020)
pattern="[\w-]+"invalid under the v flag, so silently ignoredescape: [\w\-]+
disabled on a field you need submittedvalue missing on the serverreadonly, or a hidden input
Server expects false for unchecked checkboxesthe key is missing, not falsedefault on the server, or the hidden-input trick
autocomplete="off" everywhereautofill and password managers break; ignored for logins anywayproper tokens
Radios without fieldset/legendoptions read without their questiongroup them
Paste blocked on password or email confirmfrustration, weaker passwords, WCAG failureallow paste
method="get" for sign-inpassword in the URL, history and server logspost
Relying on accept, maxlength or pattern for securitytrivially bypassedvalidate on the server
Nested formsthe inner <form> tag is dropped by the parserthe form attribute on controls
select with a disabled selected placeholdercan't return to empty; screen readers confusedvalue="" placeholder + required

Recipes

Login form

<form action="/login" method="post">
  <label for="username">Email</label>
  <input id="username" name="username" type="email"
    autocomplete="username webauthn" required
    spellcheck="false" autocapitalize="off">
 
  <label for="current-password">Password</label>
  <input id="current-password" name="password"
    type="password" autocomplete="current-password"
    required>
  <button type="button" aria-controls="current-password"
    aria-pressed="false">Show password</button>
 
  <button type="submit">Sign in</button>
  <a href="/forgot/">Forgot password?</a>
</form>

The toggle switches the field's type between password and text (and aria-pressed) in script. Keep ids stable; password managers use them as a signal. The webauthn token only does something once your script starts a conditional passkey request; without it the field behaves as a plain username field.

Sign-up with password rules

<form action="/signup" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email"
    autocomplete="email" required>
 
  <label for="new-password">Password</label>
  <input id="new-password" name="password"
    type="password" autocomplete="new-password"
    minlength="15" required
    aria-describedby="pw-hint pw-error">
  <p id="pw-hint">At least 15 characters. A passphrase
    of a few random words works well.</p>
  <p id="pw-error" hidden></p>
 
  <label>
    <input type="checkbox" name="news" value="yes">
    Send me product updates (optional)
  </label>
 
  <button type="submit">Create account</button>
</form>

Length beats composition rules: NIST SP 800-63B (revision 4, 2025) requires at least 15 characters for passwords used on their own (8 with multi-factor), forbids composition rules such as "one symbol, one capital", requires checking new passwords against a blocklist of common and breached values, and says verifiers should allow paste. Note that minlength counts UTF-16 code units, while NIST counts Unicode code points.

Address form

<fieldset>
  <legend>Delivery address</legend>
  <label for="a1">Address line 1</label>
  <input id="a1" name="line1" required
    autocomplete="shipping address-line1">
  <label for="a2">Address line 2 (optional)</label>
  <input id="a2" name="line2"
    autocomplete="shipping address-line2">
  <label for="town">Town or city</label>
  <input id="town" name="town" required
    autocomplete="shipping address-level2">
  <label for="county">County (optional)</label>
  <input id="county" name="county"
    autocomplete="shipping address-level1">
  <label for="pc">Postcode</label>
  <input id="pc" name="postcode" required size="8"
    autocomplete="shipping postal-code"
    autocapitalize="characters" spellcheck="false">
  <label for="ctry">Country</label>
  <select id="ctry" name="country" required
    autocomplete="shipping country">
    <option value="GB" selected>United Kingdom</option>
    <option value="IE">Ireland</option>
  </select>
</fieldset>

Don't validate postcode formats too strictly on the client; international formats vary, and a typeahead or lookup service is kinder than a rejected pattern.

Search form

<search>
  <form action="/search" method="get">
    <label for="q">Search the docs</label>
    <input id="q" name="q" type="search"
      enterkeyhint="search" autocomplete="off"
      spellcheck="false">
    <button type="submit">Search</button>
  </form>
</search>

method="get" makes results shareable and bookmarkable. autocomplete="off" is reasonable here: the browser's history of past queries is rarely useful in a site search.

Radio group in a fieldset

<fieldset>
  <legend>Delivery speed</legend>
  <label>
    <input type="radio" name="speed" value="std" checked>
    Standard, 3–5 days (free)
  </label>
  <label>
    <input type="radio" name="speed" value="next">
    Next day (£4.99)
  </label>
</fieldset>

Pre-select the most common safe option; a required radio group with nothing checked forces an extra click and an error.

One-time code

<label for="otp">Verification code</label>
<input id="otp" name="otp" type="text"
  inputmode="numeric" autocomplete="one-time-code"
  pattern="\d{6}" maxlength="6" required
  aria-describedby="otp-hint">
<p id="otp-hint">6 digits, sent to your phone.</p>

One field, not six boxes: paste and autofill just work. End the SMS with the origin-bound line @acme.example #123456 so Safari's autofill and Chrome's WebOTP API only offer the code on your domain.

References