Close

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.

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)

<label for="username">...</label> <input id="username">
<!-- 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

<label>Username <input type="text"></label>
<!-- 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

Use when there's no visible label text at all
<!-- 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

Use when label text exists on screen but isn't a <label> element
<!-- 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

aria-label, visually-hidden text, or sr-only span
<!-- 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

<fieldset><legend>Group label</legend> ...controls... </fieldset>
<!-- 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

<input required aria-required="true">
<!-- 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

<input aria-describedby="help-text-id error-id">
<!-- 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

<iframe title="Descriptive name of content" src="...">
<!-- 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.

Live Demo: Properly Labeled Form

Fields marked with an asterisk are required.

“Full name, required, edit text”

Enter your work email address

“Email address, required, edit text. Enter your work email address”

Format: +31 6 1234 5678

“Phone number, edit text. Format: +31 6 1234 5678”
“Your role, required, Choose a role..., combo box, collapsed”
Preferred contact method
“Preferred contact method, group. Email, radio button, not checked, 1 of 3”
Topics of interest
“Topics of interest, group. Privacy & Security, checkbox, not checked”

Maximum 500 characters

“Message (optional), edit text, multi-line. Maximum 500 characters”
“Submit registration, button”

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 (required or aria-required)?
  • Do radio button and checkbox groups have <fieldset>/<legend> wrappers?
  • Do icon-only buttons have aria-label or visually hidden text?
  • Do all iframes have descriptive title attributes?
  • 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.

Leave a Reply