Forms

Build a form somebody can fill in without seeing it — including when they get it wrong.

08of 16
≈ 13Min read
5Parts

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.

connected, not merely nearby
<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.

the only way a radio group makes sense out loud
<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

  • required is announced, and validated, without any code.
  • autocomplete lets a browser fill the field — a WCAG criterion in itself, and a large help for anyone with a motor or memory impairment.
  • inputmode chooses the on-screen keyboard; type="number" is for quantities, not for phone numbers or postcodes.
  • aria-describedby attaches the hint, so it is read after the label rather than being missed entirely.
let the browser do the work
<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.

three things the field needs when it is wrong
<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>
Colour alone is not an error

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.

Practice