Every control needs a label, and the label needs to be connected
A connected label is also a bigger click target: clicking the word focuses the field, which helps everyone on a phone. for must match the input's id, and ids have to be unique — a component rendered twice with a hard-coded id breaks both copies.
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email" required>
<!-- text sitting next to an input is not a label -->
<span>Email address</span> <input type="email">
Groups: fieldset and legend
Without the legend, each option is announced as "Standard, 5 days, radio button, 1 of 2" with no idea what the question was. The legend is read once when focus enters the group, and it is the question.
<fieldset>
<legend>How should we deliver?</legend>
<label><input type="radio" name="delivery" value="standard"> Standard, 5 days</label>
<label><input type="radio" name="delivery" value="express"> Express, next day</label>
</fieldset>
Required, and the kind of thing wanted
requiredis announced, and validated, without any code.autocompletelets a browser fill the field — a WCAG criterion in itself, and a large help for anyone with a motor or memory impairment.inputmodechooses the on-screen keyboard;type="number"is for quantities, not for phone numbers or postcodes.aria-describedbyattaches the hint, so it is read after the label rather than being missed entirely.
<input id="postcode" name="postcode" required
autocomplete="postal-code"
inputmode="text"
aria-describedby="postcode-hint">
<p id="postcode-hint">Six characters, like AZ1000.</p>
Errors somebody actually hears
aria-invalid="true" marks the state, aria-describedby attaches the message, and focus goes to the first bad field on submit. The message itself should say what to do, not that something is wrong: "Add the @" beats "Invalid input".
A summary at the top of the form — a heading, then a list of links to each bad field — is what makes a long form recoverable. Focus that summary when validation fails.
<label for="email">Email address</label>
<input id="email" type="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" class="error">Add the @ — this does not look like an email address.</p>
A red border says nothing to a screen reader and little to somebody with a colour vision deficiency. Text, plus the border, plus aria-invalid.
The rest of the form
- A
<button type="submit">— a form with no submit button cannot be submitted with Enter. - Do not disable the submit button while the form is invalid: the user is left with no way to find out why. Let them submit and tell them.
- Group related fields, keep the order logical, and put the label above the field for a single-column layout — it survives zoom.
- No timeouts on filling in a form, or a way to extend them.