/ Accessibility Fixes / Form Labels

How to Fix Form Labels

Every form control needs a programmatic label — not just a visual one. Screen readers, voice control, and form-fill tools all use the label to know what each field is for. Forms with unlabelled fields announce as "edit text, blank" — useless. This guide walks through every label pattern: visible labels, hidden labels, grouped labels for radios, and the placeholder-isn't-a-label trap. For related fixes, see the Accessibility Fixes index.

1. Audit existing forms

Step 1
Run the Accessibility audit
Run the Accessibility audit. Filter findings to form-label issues. Common errors: input with no associated label, label for pointing at non-existent id, placeholder used in place of label, label text that's empty or non-descriptive.

2. The primary label pattern

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

This pattern provides three things at once: visible label text, programmatic association via for/id, clickable label that focuses the input.

Alternative: wrapped label (no for/id needed)

<label>
  Email address
  <input type="email" name="email">
</label>

Equally valid. Choice between patterns is stylistic — wrapped is fine for simple forms, for/id is more flexible when label and input are visually separated.

3. Hidden-label patterns

Sometimes the design genuinely doesn't have room for a visible label — a search box in the nav, a single-field newsletter signup, an inline filter input. Three valid patterns:

Pattern A: aria-label

<input type="search" 
       placeholder="Search products" 
       aria-label="Search products">

Screen reader announces "Search products, edit text". Sighted users see the placeholder. Less ideal than a real label but acceptable when design demands.

Pattern B: aria-labelledby

<h2 id="search-heading">Find what you need</h2>
<input type="search" 
       aria-labelledby="search-heading">

The input borrows the heading as its label. Useful when a nearby heading already describes the field's purpose.

Pattern C: visually-hidden label

<label for="search" class="sr-only">Search products</label>
<input id="search" type="search" placeholder="Search...">

<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>

Best of both worlds: a real label element (still clickable, programmatically associated) that's hidden visually.

4. Grouped controls: fieldset + legend

Radio buttons and related checkboxes need a group label in addition to per-control labels. Fieldset and legend provide it.

<fieldset>
  <legend>Shipping method</legend>
  
  <label>
    <input type="radio" name="shipping" value="standard">
    Standard (3-5 days)
  </label>
  
  <label>
    <input type="radio" name="shipping" value="express">
    Express (next day)
  </label>
</fieldset>

Screen reader announcement: "Shipping method, group. Standard, 3-5 days, radio button, 1 of 2."

5. Common label mistakes

⚠️ Five anti-patterns to fix:

Mistake 1: Div styled to look like a label

<!-- WRONG -->
<div class="label">Email</div>
<input type="email">

<!-- RIGHT -->
<label for="email">Email</label>
<input id="email" type="email">

Mistake 2: Placeholder as label

<!-- WRONG -->
<input type="email" placeholder="Email">

<!-- RIGHT -->
<label for="email">Email</label>
<input id="email" type="email" placeholder="you@example.com">

Note placeholder still has a role — but as an EXAMPLE format, not as the label itself.

Mistake 3: Label for pointing at wrong/missing id

<!-- WRONG (typo in id, association broken) -->
<label for="emial">Email</label>
<input id="email" type="email">

<!-- RIGHT -->
<label for="email">Email</label>
<input id="email" type="email">

Mistake 4: Generic labels

"Name" alone is ambiguous in a form that asks for several names. "First name" / "Last name" / "Company name" each have their own role. Generic labels make voice control and screen readers ambiguous.

Mistake 5: Required indicator only visual

<!-- WRONG (* only visible, not announced) -->
<label for="email">Email *</label>
<input id="email" type="email">

<!-- RIGHT -->
<label for="email">Email <span aria-hidden="true">*</span></label>
<input id="email" type="email" required aria-required="true">

The required attribute is what screen readers announce. The asterisk is decorative for sighted users.

6. Float-label pattern (clean design + accessibility)

Want labels that look like placeholders but stay accessible? The float-label pattern:

<div class="float-label">
  <input id="email" type="email" placeholder=" ">
  <label for="email">Email</label>
</div>

<style>
.float-label {
  position: relative;
}
.float-label input {
  padding: 18px 12px 6px;
}
.float-label label {
  position: absolute;
  top: 16px;
  left: 12px;
  transition: all 0.2s;
  pointer-events: none;
}
.float-label input:focus + label,
.float-label input:not(:placeholder-shown) + label {
  top: 4px;
  font-size: 12px;
  color: #7c3aed;
}
</style>

Label visible initially. When user focuses or types, label animates up to make room. Real <label> element preserved — programmatic association intact.

7. Test with a screen reader

Open VoiceOver (Cmd+F5 on macOS) or NVDA (Windows free download). Tab through your form. Every field should announce:

Anything that announces just "edit text, blank" is a missing or broken label.

♿ Re-run the Accessibility audit

Verify form-label findings are cleared.

Run Accessibility Audit →
Related Guides: Accessibility Fixes  ·  Fix Form Validation  ·  Fix ARIA Labels  ·  Accessibility Guide
💬 Got a problem?