- by Wouter Nordsiek
- 0 Comments
- Guide
- 6 May 2026
Form Labels Accessibility
Guide #4 in the NGI Accessibility Series · Skill level: Beginner to Intermediate · Time: 15 to 25 minutes
Our audit of 78 NGI-funded projects found 487 form-related violations across 44 projects. That's more than half of all projects shipping forms that screen readers, voice control software, and keyboard users can't reliably operate. The most common issue? Missing or broken label associations. It's the kind of thing that takes 30 seconds to fix once you know the pattern.
Contents
1Why Labels Matter
A form label does three jobs at once. Understanding all three explains why placeholder text or a nearby heading is never an adequate substitute.
Screen readers need them. When a screen reader user tabs to an input, the screen reader announces the label text. Without a programmatically associated label, it announces something like "edit text, blank", leaving the user to guess what information you're asking for. Some screen readers attempt to guess by reading nearby text, but this is unreliable and varies between assistive technologies.
Voice control depends on them. Users of Dragon NaturallySpeaking or Voice Control on macOS say "click [label text]" to interact with form fields. If the visible label isn't programmatically connected, the voice command fails. Similarly, if the accessible name doesn't match the visible text, voice users have to guess what to say.
They expand click/tap targets. A properly associated <label> element makes the label text itself clickable. Click the label, and the input gets focus (or toggles a checkbox). This is especially helpful on mobile devices where small checkboxes and radio buttons are hard to tap precisely.
The audit breakdown: 238 missing label violations, 154 ARIA naming issues, 50 missing accessible names, and 45 validation-related labeling problems. These aren't edge cases. They're standard form fields on project homepages and documentation sites.
2Common Problems From Our Audits
2.1 Form inputs without associated labels
The single most common issue. An input sits next to visible text, but there's no programmatic connection between them.
✗ Bad: No association between text and input
Email address <input type="email" name="email">
✓ Good: Label with matching for/id
<label for="email">Email address</label> <input type="email" id="email" name="email">
2.2 Placeholder text used as the only label
Placeholders disappear once the user starts typing. Users who look away and look back have no idea what the field is for. Screen readers handle placeholder text inconsistently: some announce it, some ignore it entirely.
✗ Bad: Placeholder as only label
<input type="text" placeholder="Enter your name">
✓ Good: Visible label with placeholder as supplementary hint
<label for="fullname">Full name</label>
<input type="text" id="fullname" name="fullname"
placeholder="e.g. Jane Smith">
About "floating labels": The pattern where placeholder text animates into a small label above the field can work accessibly, but only if there's a real <label> element with a proper for/id association. The visual animation is cosmetic; the HTML structure is what matters.
2.3 Buttons without accessible names
Icon-only buttons were everywhere in the audit results: search buttons with just a magnifying glass SVG, hamburger menus, close buttons. Without text content or an ARIA label, screen readers announce them as "button" with no indication of purpose.
✗ Bad: Icon button with no accessible name
<button>
<svg viewBox="0 0 24 24">
<path d="M15.5 14h-.79l..."></path>
</svg>
</button>
<!-- Screen reader announces: "button" -->
✓ Good: Icon button with aria-label and hidden SVG
<button aria-label="Search">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l..."></path>
</svg>
</button>
<!-- Screen reader announces: "Search, button" -->
2.4 Iframes without accessible names
Embedded content (maps, videos, forms from third parties) showed up without title attributes. Screen readers announce these as "frame" with no context about what's inside.
✗ Bad: Iframe with no title
<iframe src="https://maps.example.com/embed"></iframe>
✓ Good: Iframe with descriptive title
<iframe src="https://maps.example.com/embed"
title="Office location map - Amsterdam, Netherlands">
</iframe>
2.5 Fieldsets without legends for grouped controls
Radio buttons and checkbox groups need shared context. "Yes" and "No" options are meaningless without knowing what question they answer.
✗ Bad: Radio buttons without group label
<p>Do you agree to the terms?</p> <input type="radio" name="terms" id="yes"> <label for="yes">Yes</label> <input type="radio" name="terms" id="no"> <label for="no">No</label> <!-- Screen reader on "Yes": "Yes, radio button" --> <!-- User doesn't know: yes to WHAT? -->
✓ Good: Fieldset and legend provide group context
<fieldset> <legend>Do you agree to the terms?</legend> <input type="radio" name="terms" id="yes"> <label for="yes">Yes</label> <input type="radio" name="terms" id="no"> <label for="no">No</label> </fieldset> <!-- Screen reader on "Yes": "Do you agree to the terms? group, Yes, radio button" -->
2.6 Labels that don't match visible text (WCAG 2.5.3)
When the accessible name (set via aria-label) differs from the visible text, voice control users are stuck. They see "Search" but the accessible name is "site-wide search form input", so saying "click Search" does nothing.
✗ Bad: Accessible name doesn't start with visible text
<button aria-label="Submit your registration form now"> Register </button> <!-- Voice user says "click Register" - nothing happens -->
✓ Good: Accessible name starts with visible text
<button aria-label="Register for the conference"> Register </button> <!-- Voice user says "click Register" - works -->
Rule of thumb: If there's visible text, the accessible name should start with that exact visible text. Add extra context after it if needed, never replace it.
3Implementation Guide
3a. The <label> element and for/id association
The native <label> element is the first and best option for labeling form controls. Two approaches work:
Explicit label association (recommended)
<!-- Explicit association: for attribute matches input id -->
<label for="username">Username</label>
<input type="text" id="username" name="username">
<!-- This works even when label and input are far apart in the DOM -->
<div class="form-row">
<div class="label-column">
<label for="bio">Biography</label>
</div>
<div class="input-column">
<textarea id="bio" name="bio" rows="4"></textarea>
</div>
</div>
Implicit label wrapping
<!-- Implicit: input nested inside the label -->
<label>
Username
<input type="text" name="username">
</label>
<!-- Works, but some assistive technologies handle this
less reliably. Explicit association is safer. -->
One label per input, one input per label. Don't reuse IDs, and don't point two labels at the same input (unless using aria-labelledby to combine them). Each input needs exactly one clear name.
3b. aria-label and aria-labelledby
Use ARIA labeling when a visible <label> element isn't practical. This typically means icon-only controls, or situations where the visible text isn't in a <label> element.
aria-label: Invisible label text
<!-- Search input with no visible label -->
<input type="search" aria-label="Search this site">
<button aria-label="Search">
<svg aria-hidden="true">...</svg>
</button>
<!-- Close button -->
<button aria-label="Close dialog">×</button>
<!-- Note: aria-label OVERRIDES any text inside the element.
If the button says "Go" but aria-label says "Submit form",
screen readers announce "Submit form". -->
aria-labelledby: Reference existing visible text
<!-- Heading acts as label for a section's form control -->
<h3 id="shipping-heading">Shipping Address</h3>
<input type="text" aria-labelledby="shipping-heading"
placeholder="Street address">
<!-- Combine multiple text sources -->
<span id="item-name">Blue Widget</span>
<label id="qty-label">Quantity</label>
<input type="number" aria-labelledby="qty-label item-name">
<!-- Announces: "Quantity Blue Widget" -->
<!-- aria-labelledby takes priority over <label> and aria-label.
Priority order: aria-labelledby > aria-label > <label> -->
When to use which: Use <label> for standard form inputs. Use aria-label for icon-only buttons and controls with no visible text. Use aria-labelledby when there's already visible text on the page that serves as the label.
3c. Labeling icon-only buttons
Icon-only buttons are legitimate UI. You don't need to add visible text to every button. But you do need to give them an accessible name.
Three approaches for icon buttons
<!-- Approach 1: aria-label (simplest) -->
<button aria-label="Delete item">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M6 19c0 ..."></path>
</svg>
</button>
<!-- Approach 2: Visually hidden text -->
<button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M6 19c0 ..."></path>
</svg>
<span class="sr-only">Delete item</span>
</button>
<!-- The sr-only class (screen-reader only): -->
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
<!-- Approach 3: SVG title element (least reliable) -->
<button>
<svg viewBox="0 0 24 24" role="img">
<title>Delete item</title>
<path d="M6 19c0 ..."></path>
</svg>
</button>
<!-- Some screen readers ignore <title> in SVG.
Use approach 1 or 2 for reliable support. -->
3d. Grouping with fieldset and legend
Any time you have a group of related controls that share a common question or label (radio buttons, related checkboxes, a set of address fields), wrap them in a <fieldset> with a <legend>.
Fieldset/legend for radio groups and related fields
<!-- Radio button group -->
<fieldset>
<legend>Preferred contact method</legend>
<label>
<input type="radio" name="contact" value="email"> Email
</label>
<label>
<input type="radio" name="contact" value="phone"> Phone
</label>
<label>
<input type="radio" name="contact" value="post"> Post
</label>
</fieldset>
<!-- Screen reader: "Preferred contact method, group. Email, radio button, 1 of 3" -->
<!-- Related address fields -->
<fieldset>
<legend>Billing address</legend>
<label for="street">Street</label>
<input type="text" id="street" name="street">
<label for="city">City</label>
<input type="text" id="city" name="city">
<label for="postcode">Postcode</label>
<input type="text" id="postcode" name="postcode">
</fieldset>
3e. Required field indicators
The asterisk convention (*) is widely understood visually, but you also need to communicate "required" to assistive technology and explain the convention to sighted users.
Marking required fields accessibly
<!-- Explain the convention once at the top of the form -->
<p>Fields marked with <span aria-hidden="true">*</span>
<span class="sr-only">an asterisk</span> are required.</p>
<!-- Each required field -->
<label for="email">
Email address
<span class="required-star" aria-hidden="true">*</span>
</label>
<input type="email" id="email" name="email"
required
aria-required="true">
<!-- The required attribute does double duty:
1. Triggers browser validation (prevents empty submission)
2. Most screen readers announce "required"
Adding aria-required="true" provides extra assurance
for screen readers that don't honor the HTML5 required attribute.
-->
<!-- Optional field (be explicit when most fields are required) -->
<label for="nickname">
Nickname <span class="optional-text">(optional)</span>
</label>
<input type="text" id="nickname" name="nickname">
3f. Help text with aria-describedby
Help text, format hints, and validation messages should be linked to inputs with aria-describedby. This is different from the label: it provides supplementary information, announced after the label and role.
Connecting help text and error messages
<!-- Help text linked via aria-describedby -->
<label for="password">Password</label>
<input type="password" id="password" name="password"
required aria-required="true"
aria-describedby="password-help">
<p id="password-help" class="help-text">
Must be at least 8 characters with one number and one symbol.
</p>
<!-- Screen reader: "Password, required, secure edit text.
Must be at least 8 characters with one number and one symbol." -->
<!-- Error container starts empty; content is added by JS on validation -->
<label for="email2">Email</label>
<input type="email" id="email2" name="email"
required aria-required="true"
aria-describedby="email-help email-error"
aria-invalid="false">
<p id="email-help" class="help-text">Enter your work email</p>
<p id="email-error" class="error-text" aria-live="assertive"></p>
<!-- Keep the error container in the DOM but empty.
When validation fails, set its textContent via JS.
aria-live="assertive" ensures the new text is announced.
You can reference multiple IDs in aria-describedby;
they're announced in order, separated by a pause. -->
3g. Labeling iframes
Every iframe needs an accessible name via the title attribute. This is one of the few cases where title is the correct approach. For iframes specifically, screen readers reliably announce it.
Accessible iframe examples
<!-- Video embed --> <iframe title="Project introduction video" src="https://www.youtube-nocookie.com/embed/abc123" allowfullscreen> </iframe> <!-- Contact form from a third-party service --> <iframe title="Contact form" src="https://forms.example.com/contact"> </iframe> <!-- Map embed --> <iframe title="Office location: Amsterdam Science Park" src="https://maps.example.com/embed?q=..."> </iframe> <!-- Purely decorative iframe? Hide it from assistive technology. --> <iframe src="animation.html" aria-hidden="true" tabindex="-1"> </iframe>
4Interactive Demo: Screen Reader Announcements
This form demonstrates proper labeling for every common input type. Toggle the "Show Screen Reader Announcements" button to see exactly what a screen reader announces for each field.
Notice how each field's announcement includes the label, role (edit text, combo box, radio button), state (required, checked), and any supplementary description. That's the information sighted users get from visual layout: labels, asterisks, help text. Accessible forms give the same information to every user, just through a different channel.
5Testing Checklist
Manual Tests
- Click every visible label. Does the associated input receive focus?
- Tab through the form with keyboard only. Can you reach every control?
- Does each input have a visible label that remains visible while typing?
- Are required fields indicated both visually (asterisk) and programmatically (
requiredoraria-required)? - Do radio button and checkbox groups have
<fieldset>/<legend>wrappers? - Do icon-only buttons have
aria-labelor visually hidden text? - Do all iframes have descriptive
titleattributes? - Does the accessible name of each control start with its visible text (WCAG 2.5.3)?
- Is help text connected to inputs via
aria-describedby?
Automated Testing
- Run WAVE, then check for "missing form label" and "empty button" errors
- Run axe DevTools browser extension, then filter results by "Forms" category
- Chrome DevTools: Inspect an input, check the "Accessibility" tab, then verify "Name" shows the expected label
- Firefox DevTools: Accessibility Inspector shows the computed accessible name for every element
- Run a Lighthouse accessibility audit, then look for "Form elements do not have associated labels"
Screen Reader Verification
- VoiceOver (macOS): Press VO + U, select "Form Controls" rotor. Every control should be listed with its label
- NVDA (Windows): Press Insert + F7 to list form fields, then check that each has a recognizable name
- Test at least one form end-to-end with a screen reader: fill in every field and submit
6WCAG References
| Criterion | Level | Applies To |
|---|---|---|
| 4.1.2 Name, Role, Value | A | All form controls must have an accessible name |
| 3.3.2 Labels or Instructions | A | Inputs that require user input must have labels or instructions |
| 2.5.3 Label in Name | A | Accessible name must contain the visible label text |
| 1.3.1 Info and Relationships | A | Structure and relationships (labels, groups) conveyed through presentation must also be available programmatically |
All four criteria are Level A, the baseline. Every website is expected to meet these. They're also among the easiest to fix: most violations need a single HTML attribute added or a <label> element wrapped around existing text.
