Forms are where accessibility failures cost the most: a person cannot sign up, apply or pay. Most fixes are small and improve the experience for everyone.
Learning objectives
- Associate every input with a label.
- Group related controls with
fieldsetandlegend. - Announce hints and errors with
aria-describedbyand live regions. - Meet contrast and focus requirements.
1. Labels
<!-- explicit: for + id -->
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" />
<!-- implicit: input inside label -->
<label>
Email address
<input name="email" type="email" />
</label>
Clicking the label focuses the input, screen readers announce the name, and voice control can target it.
A placeholder is not a label: it disappears on typing, has poor contrast and is not reliably announced.
<!-- wrong -->
<input placeholder="Email" />
Icon-only inputs need a visually hidden label:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
2. Groups
<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>
fieldset + legend gives the group a name. Without it, a screen reader reads three unrelated radio buttons.
3. Hints and errors
<label for="phone">Phone number</label>
<input
id="phone"
name="phone"
type="tel"
autocomplete="tel"
aria-describedby="phone-hint phone-error"
aria-invalid="true"
/>
<p id="phone-hint">Include the country code.</p>
<p id="phone-error" role="alert">Enter a 10-digit number.</p>
aria-describedbylinks both the hint and the error.aria-invalid="true"marks the field as failing.role="alert"(oraria-live="assertive") announces the error when it appears.
Do not rely on red borders alone. Add text, and keep the message next to the field.
4. Error summary and focus
On submit:
- Collect all errors.
- Render a summary at the top with links to each field.
- Move focus to the summary.
- Keep the entered values.
<div role="alert" tabindex="-1" id="error-summary">
<h2>There are 3 problems with this form</h2>
<ul>
<li><a href="#email">Email is required</a></li>
<li><a href="#phone">Enter a 10-digit number</a></li>
<li><a href="#terms">Accept the terms</a></li>
</ul>
</div>
The summary is announced once, and the user can jump to each field. Individual inline messages remain for context.
5. Required fields
<label for="name">Full name <span aria-hidden="true">*</span></label>
<input id="name" name="name" required aria-required="true" />
If you mark required fields with an asterisk, explain it in text and hide the asterisk from screen readers. Better: mark optional fields instead, so the common case needs no explanation.
6. Contrast and focus
- Body text: at least 4.5:1 against its background.
- Large text (18.66 px bold or 24 px): at least 3:1.
- UI components, borders and focus indicators: at least 3:1.
- Never remove the focus outline without a visible replacement:
:focus-visible {
outline: 3px solid #ea580c;
outline-offset: 2px;
}
- Touch targets should be at least 24×24 CSS px (44×44 is comfortable).
- Do not disable zoom: never use
user-scalable=noormaximum-scale=1.
7. Autocomplete
<input autocomplete="name" />
<input autocomplete="email" />
<input autocomplete="tel" />
<input autocomplete="street-address" />
<input autocomplete="one-time-code" />
Browsers and password managers fill forms correctly, which is itself an accessibility feature.
Common mistakes
- Placeholder-only labels.
- Errors shown only in red, with no text.
aria-invalidset but never cleared after a fix.- A
role="alert"container that is inserted empty and filled later (it may not announce) — render it with the message in one update. - Removing focus outlines globally.
- Disabling the submit button with no explanation of what is missing.
Practice
Build the accessible search form in the assignment. Then test it with a screen reader and a keyboard only.