Forms and inputs
HTML forms are the conventional mechanism for user input on the Web. The <form> element wraps input controls — <input>, <select>, <textarea>, <button> — admitting submission to a server (or interception by JavaScript). The substantial input types (text, email, password, number, date, file, checkbox, radio, etc.) admit substantial built-in validation, mobile keyboard variants, and accessibility integration. The FormData API admits programmatic form processing in JavaScript. The combination — declarative form markup, substantial input types with built-in validation, the FormData programmatic surface — is the substance of HTML’s input surface.
A simple form
<form action="/submit" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<button type="submit">Submit</button>
</form>
The principal attributes:
action— URL to submit to.method—GET(in URL) orPOST(in body).enctype— encoding (application/x-www-form-urlencodeddefault;multipart/form-datafor files;text/plainrare).novalidate— admit bypassing built-in validation.autocomplete— admit/suppress browser autocompletion.target— where to display the response (rare;_blank, etc.).
Input types
The substantial input vocabulary:
<!-- Text inputs: -->
<input type="text" name="username">
<input type="email" name="email">
<input type="password" name="pw">
<input type="search" name="q">
<input type="tel" name="phone">
<input type="url" name="website">
<!-- Numeric: -->
<input type="number" name="age" min="0" max="150" step="1">
<input type="range" name="volume" min="0" max="100" value="50">
<!-- Date and time: -->
<input type="date" name="birthday">
<input type="time" name="appointment">
<input type="datetime-local" name="event">
<input type="month" name="period">
<input type="week" name="week">
<!-- Selection: -->
<input type="checkbox" name="agree" value="yes">
<input type="radio" name="size" value="small">
<input type="radio" name="size" value="medium">
<input type="radio" name="size" value="large">
<!-- File: -->
<input type="file" name="upload">
<input type="file" name="photos" multiple accept="image/*">
<!-- Specialised: -->
<input type="color" name="bg">
<input type="hidden" name="csrf" value="token123">
<!-- Buttons: -->
<input type="submit" value="Submit">
<input type="reset" value="Reset">
<input type="button" value="Click me">
<!-- More substantial admit via <button>: -->
<button type="submit">Submit</button>
<button type="reset">Reset</button>
<button type="button">Click me</button>
The mobile keyboards adapt to type:
email— keyboard with@and.com.tel— numeric keyboard.number— numeric keyboard.url— keyboard with.comand/.
Labels
Every input must have an associated label — admits substantial accessibility:
<!-- Implicit (input inside label): -->
<label>
Name:
<input type="text" name="name">
</label>
<!-- Explicit (matched by id): -->
<label for="name">Name:</label>
<input type="text" id="name" name="name">
The label admits substantial click-target expansion (clicking the label focuses the input) and substantial screen-reader association.
For grouping form controls (radio buttons, checkboxes), <fieldset> and <legend>:
<fieldset>
<legend>Size</legend>
<label><input type="radio" name="size" value="small"> Small</label>
<label><input type="radio" name="size" value="medium"> Medium</label>
<label><input type="radio" name="size" value="large"> Large</label>
</fieldset>
Select and textarea
<!-- Dropdown: -->
<label for="country">Country:</label>
<select id="country" name="country">
<option value="">-- Select --</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="de">Germany</option>
</select>
<!-- Grouped options: -->
<select name="car">
<optgroup label="European">
<option value="vw">Volkswagen</option>
<option value="bmw">BMW</option>
</optgroup>
<optgroup label="Asian">
<option value="toyota">Toyota</option>
<option value="honda">Honda</option>
</optgroup>
</select>
<!-- Multiple selection: -->
<select name="languages" multiple size="5">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
<!-- Multi-line text: -->
<label for="comment">Comment:</label>
<textarea id="comment" name="comment" rows="5" cols="40"></textarea>
Validation
HTML5 admits substantial built-in validation:
<!-- Required: -->
<input type="text" name="name" required>
<!-- Type-based (email, url, number): -->
<input type="email" name="email" required>
<input type="number" name="age" min="0" max="150">
<input type="url" name="website">
<!-- Pattern (regex): -->
<input type="text" name="zip" pattern="\d{5}" title="5-digit ZIP code">
<!-- Length: -->
<input type="text" name="username" minlength="3" maxlength="20">
<textarea name="bio" maxlength="500"></textarea>
<!-- Numeric range: -->
<input type="number" name="rating" min="1" max="5" step="0.5">
The browser shows a built-in validation message; the :invalid and :valid CSS pseudo-classes admit substantial styling.
For programmatic validation in JavaScript:
const input = document.querySelector("input[name=email]");
input.addEventListener("invalid", (e) => {
e.preventDefault(); // suppress default popup
showCustomError(input, e.target.validationMessage);
});
// Check validity:
if (!input.checkValidity()) {
console.log(input.validationMessage);
}
// Set custom validity:
input.setCustomValidity("This email is already registered");
File uploads
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="photo" accept="image/*">
<input type="file" name="docs" multiple>
<button type="submit">Upload</button>
</form>
The enctype="multipart/form-data" is required for file uploads. The accept admits hint to the file picker; not strict validation.
For programmatic handling:
const input = document.querySelector("input[type=file]");
input.addEventListener("change", (e) => {
const files = e.target.files; // FileList
for (const file of files) {
console.log(file.name, file.size, file.type);
}
});
Autocompletion
The autocomplete admits substantial control:
<!-- Common values: -->
<input type="text" name="name" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
<input type="text" name="city" autocomplete="address-level2">
<input type="text" name="postal" autocomplete="postal-code">
<input type="text" name="country" autocomplete="country">
<!-- Credit card: -->
<input type="text" name="cc-name" autocomplete="cc-name">
<input type="text" name="cc-number" autocomplete="cc-number">
<input type="text" name="cc-exp" autocomplete="cc-exp">
<input type="text" name="cc-csc" autocomplete="cc-csc">
<!-- Login: -->
<input type="text" name="username" autocomplete="username">
<input type="password" name="pw" autocomplete="current-password">
<input type="password" name="new-pw" autocomplete="new-password">
<!-- Disable: -->
<input type="text" name="custom" autocomplete="off">
The autocomplete tokens are conventional — admit substantial password manager and form-filling integration.
FormData API
The FormData admits programmatic form serialisation:
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = new FormData(form);
// Read values:
console.log(data.get("name"));
console.log(data.get("email"));
// All entries:
for (const [key, value] of data.entries()) {
console.log(key, value);
}
// As object:
const obj = Object.fromEntries(data);
// Send via fetch:
const response = await fetch("/submit", {
method: "POST",
body: data
});
});
// Construct from scratch:
const data = new FormData();
data.append("name", "Alice");
data.append("file", fileBlob, "filename.txt");
Submission
The conventional submission via fetch:
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = new FormData(form);
const submitButton = form.querySelector('[type="submit"]');
submitButton.disabled = true; // prevent double submit
try {
const response = await fetch(form.action, {
method: form.method,
body: data
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
showSuccess(result);
} catch (err) {
showError(err);
} finally {
submitButton.disabled = false;
}
});
Common patterns
Login form
<form action="/login" method="POST">
<fieldset>
<legend>Sign in</legend>
<label for="username">Username</label>
<input type="text" id="username" name="username"
autocomplete="username" required autofocus>
<label for="password">Password</label>
<input type="password" id="password" name="password"
autocomplete="current-password" required minlength="8">
<label>
<input type="checkbox" name="remember" value="yes">
Remember me
</label>
<button type="submit">Sign in</button>
<p><a href="/forgot">Forgot password?</a></p>
</fieldset>
</form>
Search form
<form action="/search" method="GET" role="search">
<label for="q" class="sr-only">Search</label>
<input type="search" id="q" name="q" placeholder="Search..." autocomplete="off">
<button type="submit">Search</button>
</form>
Contact form
<form action="/contact" method="POST">
<fieldset>
<legend>Contact us</legend>
<label for="name">Name</label>
<input type="text" id="name" name="name" required autocomplete="name">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
<label for="topic">Topic</label>
<select id="topic" name="topic" required>
<option value="">-- Select --</option>
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="other">Other</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required minlength="10" maxlength="1000"
rows="5"></textarea>
<button type="submit">Send</button>
</fieldset>
</form>
File upload with preview
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" id="photo" name="photo" accept="image/*">
<img id="preview" alt="" hidden>
<button type="submit">Upload</button>
</form>
<script>
const input = document.getElementById("photo");
const preview = document.getElementById("preview");
input.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
preview.src = URL.createObjectURL(file);
preview.hidden = false;
}
});
</script>
Validation styling
input:invalid {
border-color: red;
}
input:valid {
border-color: green;
}
input:invalid:not(:placeholder-shown) {
background-color: #fee;
}
The :placeholder-shown admits substantial UX — only show invalid state after user has typed.
Multi-step form
<form id="checkout">
<fieldset id="step-1">
<legend>Step 1: Shipping</legend>
<!-- shipping fields -->
<button type="button" onclick="goToStep(2)">Next</button>
</fieldset>
<fieldset id="step-2" hidden>
<legend>Step 2: Payment</legend>
<!-- payment fields -->
<button type="button" onclick="goToStep(1)">Back</button>
<button type="submit">Place order</button>
</fieldset>
</form>
Custom validation
<form id="signup">
<input type="password" id="pw" name="password" required>
<input type="password" id="pw-confirm" name="password-confirm" required>
<button type="submit">Sign up</button>
</form>
<script>
const form = document.getElementById("signup");
const pw = document.getElementById("pw");
const confirm = document.getElementById("pw-confirm");
function validatePasswords() {
if (pw.value !== confirm.value) {
confirm.setCustomValidity("Passwords don't match");
} else {
confirm.setCustomValidity("");
}
}
pw.addEventListener("input", validatePasswords);
confirm.addEventListener("input", validatePasswords);
</script>
Disabled state
<input type="text" name="locked" value="readonly" readonly>
<input type="text" name="disabled" value="disabled" disabled>
<button disabled>Disabled button</button>
<fieldset disabled>
<!-- All inputs inside are disabled -->
<input type="text" name="x">
<button>Locked</button>
</fieldset>
The readonly admits the value being submitted (just not edited); disabled admits neither.
Datalist (autocomplete suggestions)
<label for="browser">Browser:</label>
<input list="browsers" id="browser" name="browser">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>
The <datalist> admits substantial autocompletion — admits typing freely with suggestions.
Output
<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
<input type="number" id="a" name="a" value="0">
+
<input type="number" id="b" name="b" value="0">
=
<output name="result" for="a b">0</output>
</form>
The <output> admits computed-result display.
Required indicators
<style>
label.required::after {
content: " *";
color: red;
}
</style>
<label for="name" class="required">Name</label>
<input type="text" id="name" name="name" required aria-required="true">
Programmatic submission
const form = document.querySelector("form");
// Submit programmatically:
form.submit(); // does NOT trigger submit event
// To trigger validation and the submit event:
form.requestSubmit(); // modern alternative
// Reset:
form.reset();
Form data validation in JavaScript
function validate(formData) {
const errors = [];
if (!formData.get("email").includes("@")) {
errors.push("Invalid email");
}
const age = parseInt(formData.get("age"));
if (isNaN(age) || age < 0 || age > 150) {
errors.push("Invalid age");
}
return errors;
}
form.addEventListener("submit", (e) => {
const data = new FormData(form);
const errors = validate(data);
if (errors.length > 0) {
e.preventDefault();
showErrors(errors);
}
});
A note on the conventional discipline
The contemporary HTML forms advice:
- Always associate
<label>with inputs — admit substantial accessibility. - Use semantic input types — admit substantial mobile keyboards and validation.
- Use
required,min,max,pattern— admit substantial built-in validation. - Use
autocompletetokens — admit substantial password manager integration. - Use
enctype="multipart/form-data"for file uploads. - Use
FormDataAPI for programmatic form processing. - Use
e.preventDefault()in submit handlers to admit fetch-based submission. - Use
<fieldset>/<legend>for grouped controls. - Use
aria-*attributes for substantial custom validation messaging. - Use
:invalid:not(:placeholder-shown)for substantial UX-friendly validation styling.
The combination — declarative form markup, substantial input types with built-in validation, the FormData programmatic surface, the substantial accessibility integration through labels and ARIA — is the substance of HTML’s input surface. The discipline produces accessible, validated, mobile-friendly forms with substantial built-in functionality.