Ir al contenido principal
Skip to docs content

Form Accessibility Deep Dive

Forms are where accessibility most directly impacts task completion. Inaccessible forms mean users literally cannot sign up, purchase, or submit information. Every interactive control needs a clear name, understandable instructions, and feedback that assistive technology can convey. This guide covers the full spectrum of form accessibility, from basic labeling through validation, grouping, and automated testing with Speakable.

Labels

Every form input needs a programmatically associated label. This is the single most important rule in form accessibility. Without a proper label, screen reader users hear only the role of the control ("edit" or "text field") with no indication of what information they should type. Visual proximity is not enough. The connection must exist in the DOM for assistive technology to discover it.

Labeling Methods

There are four approaches to associating a label with an input, each with different trade-offs for flexibility and robustness:

1.

Explicit <label for="id">

The most common and robust method. The for attribute on the label matches the id on the input. Works even when the label and input are far apart in the DOM. Clicking the label also focuses the input, improving usability for motor-impaired users.

2.

Wrapping label

Wrap the input inside the <label> element. No for/id pairing needed. The implicit association is recognized by all browsers and screen readers. Particularly useful for simple forms where label text is adjacent to the input.

3.

aria-labelledby

Points to one or more element IDs whose text content becomes the label. Useful when the visual label is complex (multiple elements), when the same label element must name multiple controls, or when the label exists but is not a <label> element.

4.

aria-label

Provides the accessible name directly as a string attribute. Used when there is no visible label at all (icon buttons, search fields with only a magnifying glass). Use sparingly; visible labels benefit everyone, not just screen reader users.

Placeholders are NOT labels

Placeholder text disappears as soon as the user starts typing, leaving them with no reference for what the field expects. Placeholders often have insufficient contrast (light gray on white). Not all screen readers announce placeholder text as the accessible name, and even when they do, the announcement is inconsistent across readers. Always use a real label in addition to any placeholder hint.

Code Example

Correct labeling patterns
<!-- Method 1: Explicit label with for/id -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" />

<!-- Method 2: Wrapping label -->
<label>
  Full name
  <input type="text" name="fullname" />
</label>

<!-- Method 3: aria-labelledby -->
<span id="phone-label">Phone number</span>
<input type="tel" aria-labelledby="phone-label" />

<!-- Method 4: aria-label (no visible label) -->
<input type="search" aria-label="Search documentation" />

<!-- ❌ WRONG: placeholder only, no label -->
<input type="email" placeholder="Enter your email" />

Screen Reader Output

When a label is properly associated, all four major screen readers announce the label text followed by the role. Without a label, users hear only the bare role with no context about what information is expected.

Screen ReaderWith LabelWithout Label
NVDA"Email address, edit""edit"
JAWS"Email address, edit""edit"
VoiceOver"Email address, text field""text field"
Narrator"Email address, edit""edit"

CLI Check

Speakable's audit mode automatically flags inputs without associated labels:

Terminal
$ speakable form.html -f audit

⚠ ISSUE: Input element has no accessible name
  → <input type="email" placeholder="Enter your email">
  Fix: Add a <label> element or aria-label attribute

Descriptions

Labels identify what a field is for, but sometimes users need additional instructions about format, constraints, or expectations. The aria-describedby attribute connects supplementary text to a form control. Screen readers announce this description after the label and role, giving users the full context without cluttering the primary name.

The announcement order is always: name → role → description. So a user focusing a password field with a description would hear the label first, then the field type, then the supplementary instructions. This layered approach ensures users get the most important information (what is this field?) immediately, with details following.

Multiple IDs can be space-separated in the aria-describedby value, and the screen reader will concatenate the text content of all referenced elements. This is useful when you have multiple constraint messages or when instructions are split across elements for styling purposes.

Code Example

Password field with description
<label for="password">Password</label>
<input
  type="password"
  id="password"
  aria-describedby="pw-instructions pw-strength"
/>
<p id="pw-instructions">Must be at least 8 characters</p>
<p id="pw-strength">Include one uppercase letter and one number</p>

Screen Reader Output

All major screen readers announce the description after the name and role. The description is read as a continuous string, combining the text of all referenced elements.

Screen ReaderAnnouncement
NVDA"Password, edit, Must be at least 8 characters Include one uppercase letter and one number"
JAWS"Password, edit, Must be at least 8 characters Include one uppercase letter and one number"
VoiceOver"Password, secure text field, Must be at least 8 characters Include one uppercase letter and one number"
Narrator"Password, edit, Must be at least 8 characters Include one uppercase letter and one number"

Required Fields

Users need to know which fields are mandatory before they submit a form. The HTML required attribute provides both native browser validation and screen reader announcement. When a screen reader encounters a required field, it appends "required" to the field information, making it clear the user cannot skip it.

Alternatively, aria-required="true" provides the same announcement without native validation behavior. This is useful when you handle validation in JavaScript and don't want the browser's default validation UI.

Don't rely only on visual indicators

A red asterisk (*) next to a field label communicates "required" visually, but screen reader users may never encounter it, or may hear "star" or "asterisk" without understanding its meaning. Always pair visual indicators with programmatic marking via required or aria-required.

Code Example

Required field patterns
<!-- Native required with browser validation -->
<label for="email">Email address</label>
<input type="email" id="email" required />

<!-- ARIA required without native validation -->
<label for="username">Username</label>
<input type="text" id="username" aria-required="true" />

<!-- Visual + programmatic indicator together -->
<label for="phone">
  Phone number <span aria-hidden="true" class="text-red-500">*</span>
</label>
<input type="tel" id="phone" required />

Screen Reader Output

Screen ReaderAnnouncement
NVDA"Email address, edit, required"
JAWS"Email address, edit, required"
VoiceOver"Email address, required, text field"
Narrator"Email address, edit, required"

Validation Errors

When a user submits a form with errors, they need immediate, actionable feedback. For screen reader users, this means the error must be programmatically associated with the field and announced without requiring the user to hunt for it. The combination of aria-invalid="true" and an error message linked via aria-describedby creates a robust error reporting pattern.

Setting aria-invalid="true" on a field causes screen readers to announce "invalid" when the field receives focus. The error message connected via aria-describedby is read immediately after, giving the user the full picture: what went wrong and what to fix.

For inline validation that fires as the user types (or on blur), use role="alert" or aria-live="polite" on the error message container so the error is announced immediately when it appears, without requiring the user to move focus.

Code Example

Inline validation pattern
<!-- Before submission: field is valid -->
<label for="email">Email address</label>
<input type="email" id="email" required aria-describedby="email-error" />
<p id="email-error"></p>

<!-- After failed validation: mark invalid + show error -->
<label for="email">Email address</label>
<input
  type="email"
  id="email"
  required
  aria-invalid="true"
  aria-describedby="email-error"
/>
<p id="email-error" role="alert" class="text-red-600">
  Please enter a valid email address
</p>

<!-- Real-time validation with aria-live -->
<label for="username">Username</label>
<input type="text" id="username" aria-describedby="username-feedback" />
<div id="username-feedback" aria-live="polite">
  <!-- Dynamically populated as user types -->
  Username is already taken
</div>

Screen Reader Output

When the user tabs to an invalid field, screen readers combine the invalid state with the error description into a single announcement:

Screen ReaderAnnouncement
NVDA"Email address, edit, invalid, Please enter a valid email address"
JAWS"Email address, edit, invalid entry, Please enter a valid email address"
VoiceOver"Email address, invalid data, text field, Please enter a valid email address"
Narrator"Email address, edit, invalid, Please enter a valid email address"
See Also: Live Regions & Dynamic Content

Learn how aria-live regions work for real-time validation messages that update without page reload.

Fieldsets and Legends

When a form contains groups of related controls (radio buttons for a single choice, checkboxes for multiple selections, or a set of address fields), the <fieldset> and <legend> elements provide essential group context. Without this grouping, screen reader users navigating between radio buttons hear only the individual option label with no context about what question they're answering.

The legend text is prepended to each control's individual announcement. When a user focuses a radio button inside a fieldset, the screen reader says the legend first, then the option label, then the role. This gives complete context for every individual control without the user needing to navigate back to read a heading or question.

Without the fieldset grouping, a user tabbing through radio buttons hears only "Express, radio button". They have no idea what "Express" refers to. Is it a shipping method? A payment plan? A subscription tier? The legend provides the missing context that makes each option understandable in isolation.

Code Example

Fieldset with legend
<!-- ✅ Grouped with fieldset and legend -->
<fieldset>
  <legend>Shipping method</legend>
  <label>
    <input type="radio" name="shipping" value="standard" />
    Standard (5-7 days)
  </label>
  <label>
    <input type="radio" name="shipping" value="express" />
    Express (2-3 days)
  </label>
  <label>
    <input type="radio" name="shipping" value="overnight" />
    Overnight (next day)
  </label>
</fieldset>

<!-- ❌ Without fieldset - no group context -->
<p>Shipping method</p>
<label>
  <input type="radio" name="shipping" value="standard" />
  Standard (5-7 days)
</label>
<label>
  <input type="radio" name="shipping" value="express" />
  Express (2-3 days)
</label>

Screen Reader Output

The difference between grouped and ungrouped controls is dramatic. With a fieldset, users always know the context of each option:

Screen ReaderWith FieldsetWithout Fieldset
NVDA"Shipping method, grouping. Express (2-3 days), radio button""Express (2-3 days), radio button"
JAWS"Shipping method, group. Express (2-3 days), radio button""Express (2-3 days), radio button"
VoiceOver"Shipping method, group. Express (2-3 days), radio button""Express (2-3 days), radio button"
Narrator"Shipping method, group. Express (2-3 days), radio button""Express (2-3 days), radio button"

Autocomplete

The autocomplete attribute tells browsers and assistive technology what type of data a field expects. This enables password managers to fill credentials, browsers to offer stored addresses, and mobile keyboards to suggest appropriate input methods. For users with motor impairments or cognitive disabilities, autofill dramatically reduces the physical and mental effort required to complete forms.

Beyond convenience, autocomplete reduces errors. Instead of typing a 16-digit credit card number (with opportunities for typos), the browser fills it from stored data. Instead of remembering an exact address format, the browser provides the previously saved version. This benefits everyone, but the impact is particularly significant for users who find typing difficult or error-prone.

Common Values

ValuePurpose
nameFull name
emailEmail address
telTelephone number
address-line1Street address line 1
address-line2Street address line 2
postal-codeZIP or postal code
cc-numberCredit card number
cc-expCredit card expiry date
current-passwordCurrent password (login forms)

Code Example

Checkout form with autocomplete
<form>
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" autocomplete="name" />

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

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

  <label for="address">Street address</label>
  <input type="text" id="address" autocomplete="address-line1" />

  <label for="city">City</label>
  <input type="text" id="city" autocomplete="address-level2" />

  <label for="zip">ZIP code</label>
  <input type="text" id="zip" autocomplete="postal-code" />

  <label for="cc">Card number</label>
  <input type="text" id="cc" autocomplete="cc-number" inputmode="numeric" />

  <label for="cc-exp">Expiry</label>
  <input type="text" id="cc-exp" autocomplete="cc-exp" />
</form>

Testing Forms with Speakable

Speakable provides several modes for auditing form accessibility. The audit format catches structural issues like missing labels and landmark problems, while the text format lets you hear exactly what screen readers will say for each form control. Combining both gives you comprehensive coverage: structural correctness plus perceptual accuracy.

Audit Mode

Run the audit to catch missing labels, incorrect ARIA usage, and landmark issues across your entire form:

Terminal
$ speakable form.html -f audit

Targeted Field Testing

Use a CSS selector to hear what all four screen readers would announce for every input, select, and textarea in your form:

Terminal
$ speakable form.html -f text -s all --selector "input, select, textarea"

Error State Testing

Check how error states are announced by targeting elements with aria-invalid:

Terminal
$ speakable form.html -f text -s nvda --selector "[aria-invalid]"

Example Output

Here's what audit output looks like for a form with accessibility issues versus a clean form:

Form with issues
$ speakable signup-form.html -f audit

ACCESSIBILITY AUDIT: signup-form.html
══════════════════════════════════════

ISSUES FOUND: 4

✗ ERROR: Input has no accessible name
  → <input type="email" placeholder="Email">
  Fix: Add a <label> with matching for/id or use aria-label

✗ ERROR: Input has no accessible name
  → <input type="password" placeholder="Password">
  Fix: Add a <label> with matching for/id or use aria-label

⚠ WARNING: Required field has no programmatic indicator
  → <input type="email" placeholder="Email" class="required">
  Fix: Add the required attribute or aria-required="true"

⚠ WARNING: Radio group not wrapped in fieldset
  → <input type="radio" name="plan" value="free">
  Fix: Wrap related radio buttons in <fieldset> with <legend>
Clean form - no issues
$ speakable checkout-form.html -f audit

ACCESSIBILITY AUDIT: checkout-form.html
════════════════════════════════════════

ISSUES FOUND: 0

✓ All 8 form controls have accessible names
✓ Required fields are programmatically marked
✓ Fieldsets used for grouped controls
✓ Error messages linked via aria-describedby
✓ Autocomplete attributes present on identity fields

SCORE: 100/100

Form Accessibility Checklist

Use this quick reference to ensure your forms meet accessibility requirements. Each item represents a pattern that directly impacts whether assistive technology users can successfully complete your form.

Every input, select, and textarea has a programmatically associated label (not just a placeholder)

Supplementary instructions use aria-describedby to connect to the relevant field

Required fields use the required attribute or aria-required="true"

Validation errors set aria-invalid="true" and link error text via aria-describedby

Related controls (radios, checkboxes) are wrapped in <fieldset> with a <legend>

Identity and payment fields include appropriate autocomplete values

Dynamic validation messages use role="alert" or aria-live for immediate announcement

Form has been tested with speakable -f audit and all issues resolved

Related Pages