Constructing forms
A form is a conversation. Every box needs a name, every problem needs words, and every group needs a title. Get those three right and most people, including screen reader users, can finish the form without help.
In one minute
- Name every control with a visible
<label for>. A placeholder is a hint at best, not a label. - Say what is wrong in text, next to the field, and link it with
aria-describedbyandaria-invalid. Colour alone is not enough. - Group related choices (radios, checkboxes) in a
<fieldset>with a<legend>. - Long forms put the step in the heading and the URL, and move focus to the new step's heading.
Picture a paper form handed to someone who cannot see it, and read aloud one box at a time. If the clerk says only "box, box, box", nobody can fill it in. A label is the clerk saying "Work email". An error is the clerk saying "That one is wrong, because it needs an @ sign". A legend is "Now the delivery questions".
1. The label is the name, not the placeholder
Browsers build a list of every control on the page, with a name for each. Assistive technology reads that name
out. This list is called the accessibility tree. In plain words, it is the page as a screen reader sees it: roles,
names and states instead of pixels. A <label for> supplies the name. It also makes the label
clickable, which enlarges the tap target (WCAG 2.5.8 asks for at least 24 by 24 CSS pixels).
Lab: placeholder or label?
Step 1. Pick a version, then type a few letters into the box.
With the placeholder version, the question vanishes the moment you type. People with memory or attention difficulties lose track of what the box wants. Placeholder text is also usually low contrast (WCAG 1.4.3 needs 4.5:1) and some screen readers skip it. Zoom makes it worse: at 400% a long placeholder is cut off.
2. Errors need words, and a link to the field
A red border tells only people who can see red. WCAG 1.4.1 says colour must not be the only signal, and 3.3.1 says an
error must be described in text. Three attributes do the work: aria-invalid="true" marks the field,
aria-describedby points at the message, and role="alert" announces it as it appears.
Lab: submit the form with a bad email
Step 2. Choose a version, type nothing (or "ada"), then press Create account.
- Put the message next to the field, in plain words that say how to fix it.
- Move focus to the first invalid field on submit. With several errors, a summary at the top that links to each field also helps.
- Keep
autocomplete="email"and similar tokens. They let the browser fill known data (WCAG 1.3.5) and help people who find typing hard. - Use
requiredoraria-required="true"so the "required" fact is announced, and also say it in words in the label.
3. Groups need a legend
Two radio buttons named "Pickup" and "Ship" make no sense alone: pickup of what? A <fieldset> wraps the
group and its <legend> is read before each option, so the question travels with the answers.
Lab: tab into the radio group
Step 3. Pick a version, then press Tab into the options and use the arrow keys.
Arrow keys move between radios in one group and Tab leaves the group. That works only when the radios share a
name. Use a fieldset for checkbox groups too. Do not nest a fieldset for a single control.
4. Multi-step forms: tell the user where they are
When you swap in the next step, the button the user just pressed often disappears. The browser then drops focus back to
the top of the page, so a keyboard user starts again and a screen reader user hears nothing. The fix is small: put the
step in the heading (for example "Step 2 of 3: Delivery"), and move focus to that heading. Give the heading
tabindex="-1" so script can focus it without adding a tab stop.
Lab: press Next step
Step 4. Pick a version. Press Tab to the button, then Enter. Watch where focus lands.
Also put the step in the URL (for example ?step=2) so Back works and a refresh does not lose the place.
Keep what the user typed when they go back. WCAG 3.3.7 (Redundant Entry) says do not ask for the same information twice.
Common mistakes
| You see | Why it is a problem | Fix |
|---|---|---|
| Placeholder used as the label | Disappears while typing; low contrast; unreliable name. | <label for> that stays visible. |
| Red border, no message | Colour-blind and screen reader users get nothing. | Text message + aria-invalid + aria-describedby. |
| Error shown only in a toast that fades | Gone before it can be read or found. | Keep it in the page next to the field. |
| Radios with no legend | Options are heard without the question. | fieldset + legend. |
| Next step drops focus | Keyboard user restarts from the top. | Focus the new step's heading. |
| Blocking paste in password or email boxes | Breaks password managers (WCAG 3.3.8). | Allow paste and autofill. |
- In lab 2, submit the form empty with "Red border only". Then switch to "Text + linked" and submit again. Compare the readout.
- Press Tab through lab 3 with no fieldset, and read what you would hear for "Ship".
- Zoom your browser to 400%. Is every label still visible next to its box?
Remember
- A visible
<label for>is the name. Placeholders are hints. - Errors are text, next to the field, linked by
aria-describedby, witharia-invalidand a move of focus. - Related controls share a
fieldsetandlegend. - Multi-step forms: step in the heading and URL, focus on the heading.