Frontend / Accessibility / 05_forms_accessibility.md

Accessible Forms

Updated 4 min read source
On this page6
  1. TL;DR
  2. In depth
  3. Gotchas / edge cases
  4. What a senior is expected to say
  5. Cross-references
  6. Further reading

Accessible Forms

TL;DR

Forms are where accessibility most often breaks and where it matters most (sign-up, checkout, search). The essentials: every control has a programmatically associated <label> (a placeholder is not a label), errors are announced and tied to their field via aria-describedby + aria-invalid, related controls are grouped with <fieldset>/<legend>, and autocomplete attributes let browsers/AT fill fields. Most of this is native HTML — see the Constraint Validation API in HTML for the validation half.

In depth

How do you correctly label a form control?

Associate a <label> with the control’s id, or wrap the control:

html
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" />

<!-- or wrapping -->
<label>Email <input type="email" autocomplete="email" /></label>

A correct label is clickable (bigger hit target), announced by screen readers, and required by WCAG. For an icon-only control with no visible text, use aria-label — but visible labels are better for everyone.

Why isn’t a placeholder a label?

placeholder disappears on input (so users forget what the field is), has poor contrast by default, isn’t reliably announced as the name, and can’t be relied on for instructions. Placeholder = optional example/format hint; label = always-visible name. Placeholder-only forms are a classic accessibility (and usability) failure.

How do you make validation errors accessible?

Three parts:

  1. Associate the error text with the field: aria-describedby points at the error element’s id.
  2. Mark invalid: aria-invalid="true" on the field.
  3. Announce it: render the error in a live region (or role="alert") so it’s spoken when it appears.
html
<label for="pw">Password</label>
<input id="pw" type="password" aria-invalid="true" aria-describedby="pw-err" />
<p id="pw-err" role="alert">Must be at least 8 characters.</p>

For a submit-time summary, render a role="alert" list of errors and move focus to it (or to the first invalid field). Don’t rely on color alone to show “this field is wrong” (WCAG 1.4.1).

When do you use <fieldset> and <legend>?

To group related controls and give the group a name — essential for radio-button sets and checkbox groups:

html
<fieldset>
  <legend>Shipping speed</legend>
  <label><input type="radio" name="speed" value="std" /> Standard</label>
  <label><input type="radio" name="speed" value="exp" /> Express</label>
</fieldset>

Screen readers announce the legend with each option (“Shipping speed, Standard, radio button”), so the question is clear. Without it, the radios are just floating options.

What does the autocomplete attribute do for accessibility?

Standard autocomplete tokens (email, name, tel, street-address, cc-number, one-time-code…) let browsers and AT recognize a field’s purpose and autofill it (WCAG 1.3.5 Identify Input Purpose). This reduces effort dramatically for users with motor or cognitive disabilities. Use the correct token, not autocomplete="off" on fields users would want filled.

How do you indicate required and optional fields accessibly?

Use the native required attribute (it’s exposed to AT and triggers native validation); aria-required="true" for custom controls that can’t use it. Mark requiredness in the visible label text too (don’t convey it by asterisk-color alone), and state up front whether * means required.

Gotchas / edge cases

  • for/id mismatch silently breaks the association — the label looks fine but isn’t connected. Test by clicking the label: focus should move to the input.
  • One label for multiple inputs (split phone/date fields) — give each input its own accessible name (aria-label or grouped with a <fieldset>/<legend>).
  • Disabled vs aria-disableddisabled removes the control from the tab order and form submission; aria-disabled="true" keeps it focusable (so users can discover it) but you must block its action in JS. Choose deliberately.
  • Error appears but isn’t announced — if the error node is added to the DOM at the same instant, some AT misses it; prefer an always-present live region you populate, or role="alert".
  • Custom dropdowns/date pickers lose all of this unless you rebuild it — prefer native <select>/<input type="date"> or an APG-compliant component (Accessible Components (APG Patterns)).
  • Real-time validation that fires on every keystroke spams live-region announcements — validate on blur/submit, or debounce announcements.

What a senior is expected to say 4

  • “Every control has an associated <label> — placeholders are hints, not labels. Errors use aria-invalid + aria-describedby and a live region/role=alert, and never color alone.”
  • “Radio/checkbox groups go in <fieldset>/<legend> so the question is announced with each option.”
  • “Correct autocomplete tokens satisfy 1.3.5 and cut effort for everyone.”
  • “Native required over custom; on submit I move focus to the error summary or first invalid field.”

Cross-references

Further reading