frontend / accessibility / 05_forms_accessibility.md

Accessible Forms

4 min read source

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 ../01_html/ for the validation half.

Interview Q&A

Q: How do you correctly label a form control?

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

<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.

Q: Why isn’t a placeholder a label?

A: 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.

Q: How do you make validation errors accessible?

A: 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.
<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).

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

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

<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.

Q: What does the autocomplete attribute do for accessibility?

A: 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.

Q: How do you indicate required and optional fields accessibly?

A: 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 (06_accessible_components.md).
  • 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

  • “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