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:
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.
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.
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.
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
<!-- 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 Reader | With Label | Without 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:
$ 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
<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 Reader | Announcement |
|---|---|
| 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
<!-- 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 Reader | Announcement |
|---|---|
| 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
<!-- 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 Reader | Announcement |
|---|---|
| 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" |
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
<!-- ✅ 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 Reader | With Fieldset | Without 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
| Value | Purpose |
|---|---|
| name | Full name |
| Email address | |
| tel | Telephone number |
| address-line1 | Street address line 1 |
| address-line2 | Street address line 2 |
| postal-code | ZIP or postal code |
| cc-number | Credit card number |
| cc-exp | Credit card expiry date |
| current-password | Current password (login forms) |
Code Example
<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:
$ 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:
$ 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:
$ 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:
$ 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>
$ 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
Live Regions
Dynamic content announcements for real-time validation and status updates.
Common Mistakes
The most frequent accessibility issues and how to fix them.
Testing Checklist
Step-by-step checklist for verifying accessibility across components.
Component Patterns
Accessible implementations for common interactive UI patterns.