Forms, Validation, and UX
Input types, constraints, and accessible form design
HTML forms collect user input and submit it to a server. Modern HTML provides rich input types and built-in validation that catches errors before data leaves the browser, reducing server load and improving user experience.
Form Structure
<form action="/signup" method="post" novalidate>
<div>
<label for="email">Email address</label>
<input
id="email"
type="email"
name="email"
required
autocomplete="email"
placeholder="you@example.com"
>
<span class="error" aria-live="polite"></span>
</div>
<button type="submit">Sign up</button>
</form>
Input Types
Using the correct type triggers appropriate mobile keyboards and enables built-in validation.
<input type="email" name="email"> <!-- validates email format -->
<input type="url" name="website"> <!-- validates URL format -->
<input type="tel" name="phone"> <!-- numeric keypad on mobile -->
<input type="number" name="age" min="18" max="120">
<input type="date" name="dob">
<input type="password" name="pwd" minlength="8">
<input type="search" name="q"> <!-- shows clear button -->
<input type="range" name="vol" min="0" max="100" step="5">
<input type="color" name="brand">
<input type="file" name="avatar" accept="image/*" multiple>
<input type="checkbox" name="agree" required>
<input type="radio" name="plan" value="free">
Built-in Validation Attributes
<input type="text"
required <!-- must not be empty -->
minlength="3" maxlength="50" <!-- character count -->
pattern="[A-Za-z]+" <!-- regex constraint -->
title="Letters only" <!-- shown in native error bubble -->
>
<input type="number" min="1" max="99" step="1">
Custom Validation with the Constraint Validation API
Use novalidate on the form and implement your own validation loop for consistent cross-browser styling and better error messages.
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
let valid = true;
form.querySelectorAll('[required]').forEach(input => {
const errorEl = input.nextElementSibling;
if (!input.validity.valid) {
valid = false;
errorEl.textContent = getErrorMessage(input);
input.setAttribute('aria-invalid', 'true');
} else {
errorEl.textContent = '';
input.removeAttribute('aria-invalid');
}
});
if (valid) form.submit();
});
function getErrorMessage(input) {
if (input.validity.valueMissing) return 'This field is required.';
if (input.validity.typeMismatch) return 'Please enter a valid ' + input.type + '.';
if (input.validity.tooShort) return 'Minimum ' + input.minLength + ' characters.';
if (input.validity.patternMismatch) return input.title || 'Invalid format.';
return 'Invalid value.';
}
Fieldsets and Select Elements
<fieldset>
<legend>Preferred contact method</legend>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
<select name="country" required>
<option value="">Select a country…</option>
<optgroup label="North America">
<option value="us">United States</option>
<option value="ca">Canada</option>
</optgroup>
</select>
Best Practices
- Always pair
<label for="id">with every input — required for screen readers - Use
autocompleteattributes to enable browser autofill - Place error messages adjacent to the field, not only in a summary at the top
- Never rely on client-side validation alone — validate on the server too
- Use
aria-describedbyto link inputs to their error messages