Accessible forms

A form is a task, not just content. To finish it, a person has to know what each field is for, find out when something is wrong and how to fix it, understand how the choices relate, and — ideally — let the browser fill in the data it already holds. Break any one of those and the task stalls: a field announced as bare “edit text”, an error spoken only in red, a group of radios with no shared question, or a name field the browser can’t autofill all push effort back onto the user, and for some people they close the door entirely.

This lesson works through the four defects behind the majority of real-world form failures. Each one is small to fix and uses plain HTML you already have — the wins come from wiring the parts together correctly, not from adding ARIA on top.

What you’ll learn

How to give every input a real, programmatic label; how to associate an error with its field and announce it the moment it appears; how to group related radios and checkboxes with fieldset and legend; and how to add the right autocomplete tokens so personal-data fields fill themselves.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 1.3.1 Info and Relationships A A field’s label, its grouping, and its error are conveyed in the markup, not by visual layout alone.
WCAG 2.2 3.3.1 Error Identification A When input is rejected, the field in error is identified and the problem is described in text.
WCAG 2.2 3.3.2 Labels or Instructions A Labels or instructions are provided when content requires user input.
WCAG 2.2 3.3.3 Error Suggestion AA If a fix is known, it is suggested to the user, unless doing so risks security.
WCAG 2.2 1.3.5 Identify Input Purpose AA The purpose of fields collecting user data is programmatically set (autocomplete tokens).
WCAG 2.2 4.1.2 Name, Role, Value A Each field exposes a name and role; its state and value are available to assistive technology.
WCAG 2.2 4.1.3 Status Messages AA Status and error messages are exposed to assistive tech without moving focus.
EN 301 549 9.3.3 / 9.4.1.2 (incorporates WCAG) European harmonised standard; references the WCAG A/AA set including form criteria.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA, including labels and errors.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA.

The four problems we’ll fix

Each card below isolates one common form defect. For every issue you get a plain-language statement of the problem, a Bad example (shown as escaped, non-running code so it can’t harm this page), a Good example, the copyable Code, and an ordered fix.

Input with no real label

WCAG 2.2 · 1.3.1 A 3.3.2 A 4.1.2 A EN 301 549

A placeholder is not a label. It’s grey, low-contrast hint text that disappears the instant the user types, so it fails as an instruction and is unreliable as a name. A field with only a placeholder is announced by many screen readers as just “edit text”, and once the user starts typing they have nothing left to remind them what the field was for. A real <label> stays visible, is read on focus, and — because clicking it moves focus into the field — gives motor and touch users a larger target.

Bad

The placeholder is doing the label’s job. There is no programmatic name, and the hint vanishes the moment the user types.

bad-placeholder-label.html
<input type="email" name="email" placeholder="Email">

Good

A visible <label> is tied to the input by matching for and id. The placeholder, if kept at all, only shows an example format — it never replaces the label.

good-label.html
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="name@example.com">

Code

When the design genuinely has no room for a visible label, use a visually hidden one — never aria-label alone if a real label can be shown. A visible label helps everyone, including sighted cognitive users.

visually-hidden-label.html
<label for="site-search" class="visually-hidden">Search the site</label>
<input type="search" id="site-search" name="q" placeholder="Search…">

How to fix

  1. Add a <label> for every field and tie it to the input with matching for and id values.
  2. Keep the label visible. Use a visually-hidden label only when the design truly can’t show text.
  3. Use placeholder only for an example of the expected format, never as the field’s name or its only instruction.
  4. Verify in the accessibility tree that the field is announced with its label and role, e.g. “Email, edit text”.

Error message not associated or announced

WCAG 2.2 · 3.3.1 A 3.3.3 AA 4.1.3 AA EN 301 549

An error shown only as red text near a field doesn’t reach a screen reader user: it isn’t part of the field’s name, and nothing tells assistive technology that anything changed. The user submits, focus stays put, and the page looks identical to them. Three things have to be true — the error text must be associated with its field via aria-describedby, the field must be marked aria-invalid="true", and the failure must be announced, either by moving focus to an error summary with role="alert" or by rendering errors into a live region.

Bad

The message is visually next to the field but not connected to it. It isn’t announced on focus, isn’t announced on submit, and the field isn’t marked invalid.

bad-error.html
<label for="pw">Password</label>
<input type="password" id="pw" name="pw">
<span class="error">Password is required</span>

Good

The field points at its error with aria-describedby and is marked aria-invalid="true", so the message is read on focus. The error text describes how to fix the problem (3.3.3), not just that something is wrong.

good-error.html
<label for="pw">Password</label>
<input type="password" id="pw" name="pw"
       aria-describedby="pw-error" aria-invalid="true">
<span class="error" id="pw-error">
  Enter a password of at least 8 characters.
</span>

Code

On submit, render an error summary, move focus to it so it’s read, and link each item to its field. Give the summary role="alert" (or tabindex="-1" plus a script focus) so failure is announced without relying on the user to go hunting (4.1.3).

error-summary.html
<div role="alert" tabindex="-1" id="error-summary">
  <h2>There is a problem</h2>
  <ul>
    <li><a href="#pw">Enter a password of at least 8 characters.</a></li>
  </ul>
</div>
<!-- After render: errorSummary.focus(); -->

How to fix

  1. Connect each error to its field with aria-describedby pointing at the error element’s id.
  2. Set aria-invalid="true" on the field while it’s in error, and remove it once the value is valid.
  3. Write the message as a fix, not just a flag — say what to enter (3.3.3).
  4. On submit, show an error summary, move focus to it (or give it role="alert"), and link each entry to the field it describes.
  5. Don’t rely on colour alone; pair red with an icon or text so 1.4.1 is met too.

Related controls not grouped

WCAG 2.2 · 1.3.1 A 3.3.2 A EN 301 549 Section 508

A set of radio buttons or checkboxes answers a single question — “Delivery speed?”, “Which notifications?”. Each control has its own label, but the question that ties them together is usually just plain text sitting above them, invisible to the accessibility tree. A screen reader user arriving on the third radio hears “Express, radio button, 2 of 3” with no idea what is being chosen. Wrapping the set in a <fieldset> with a <legend> makes the group’s question part of each control’s context.

Bad

The grouping question is a bare paragraph. The radios are only related visually, so the relationship is lost to assistive technology (1.3.1).

bad-group.html
<p>Delivery speed</p>
<label><input type="radio" name="speed" value="standard"> Standard</label>
<label><input type="radio" name="speed" value="express"> Express</label>

Good

A <fieldset> wraps the set and a <legend> states the question. Screen readers announce the legend with each option, so the choice always has context.

good-fieldset.html
<fieldset>
  <legend>Delivery speed</legend>
  <label><input type="radio" name="speed" value="standard"> Standard</label>
  <label><input type="radio" name="speed" value="express"> Express</label>
</fieldset>

Code

The same pattern works for a checkbox group. If a native fieldset can’t be used (for example with custom layout), recreate it with role="group" and aria-labelledby pointing at the group’s heading.

group-role.html
<div role="group" aria-labelledby="notify-legend">
  <p id="notify-legend">Email me about</p>
  <label><input type="checkbox" name="notify" value="news"> Product news</label>
  <label><input type="checkbox" name="notify" value="tips"> Tips</label>
</div>

How to fix

  1. Wrap each set of related radios or checkboxes in a <fieldset>.
  2. Put the group’s question in a <legend> as the first child of the fieldset.
  3. Keep an individual <label> on every control as well — the legend names the group, the label names the option.
  4. If you can’t use a native fieldset, use role="group" with aria-labelledby referencing the visible question.
  5. Reserve fieldsets for genuinely related controls; don’t wrap every single input, or the extra announcements become noise.

Missing autocomplete on personal-data fields

WCAG 2.2 · 1.3.5 AA EN 301 549 ADA Title II

Identify Input Purpose asks that fields collecting a user’s own information declare what they hold, using the standard autocomplete tokens. With the right token, the browser and assistive tools can fill the field automatically and, increasingly, show a personalised icon next to it. That removes the burden of remembering and re-typing personal data — which matters most for people with cognitive, motor, or memory disabilities. The tokens are a fixed vocabulary (such as name, email, tel), not arbitrary strings.

Bad

These fields collect the user’s own contact details but declare no purpose, so the browser can’t reliably autofill them (1.3.5).

bad-autocomplete.html
<label for="fn">Full name</label>
<input type="text" id="fn" name="fn">

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

Good

Each field declares its purpose with the correct token. The type still drives the right keyboard and validation; the autocomplete token drives autofill and input-purpose support.

good-autocomplete.html
<label for="fn">Full name</label>
<input type="text" id="fn" name="fn" autocomplete="name">

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

<label for="ph">Phone</label>
<input type="tel" id="ph" name="ph" autocomplete="tel">

Code

Tokens can be qualified for multi-part data, and you can scope them to a contact type. Use autocomplete="off" only where autofill would be wrong, such as a one-time code.

autocomplete-tokens.html
<input autocomplete="given-name">       <!-- first name -->
<input autocomplete="family-name">      <!-- last name -->
<input autocomplete="street-address">
<input autocomplete="postal-code">
<input autocomplete="work email">       <!-- scoped to work contact -->
<input autocomplete="one-time-code">    <!-- SMS code field -->

How to fix

  1. Add an autocomplete attribute to every field that collects the user’s own data — name, email, phone, address, and so on.
  2. Use the exact tokens from the HTML autofill vocabulary (name, email, tel, given-name, postal-code…), not invented values.
  3. Set the matching type as well so the right keyboard and validation appear on mobile.
  4. Only use autocomplete="off" where autofill is genuinely inappropriate; never use it to defeat password managers.

Recap

  • Give every input a real, programmatic label — a <label for> tied to the field’s id. A placeholder is not a label (1.3.1, 3.3.2, 4.1.2).
  • Wire each error to its field with aria-describedby, mark the field aria-invalid="true", and announce errors through a focused summary or a live region (3.3.1, 3.3.3, 4.1.3).
  • Group related radios and checkboxes in a <fieldset> with a <legend> that asks the question (1.3.1, 3.3.2).
  • Add the correct autocomplete tokens to personal-data fields so the browser can fill them (1.3.5).

The same fixes satisfy WCAG, EN 301 549, Section 508, and ADA Title II at once — wire each field correctly and you meet them all.