Skip to main content

How to Fix Missing or Incorrect Form Labels

Your accessibility scan flagged one or more form fields that lack proper labels. This is among the most common accessibility failures on the web — and one of the most impactful to fix. Without labels, screen reader users cannot identify what a form field is for, speech input users cannot target fields by name, and all users lose the click-to-focus behavior that labels provide.

What the Scanner Found

The scan uses axe-core rules to detect form labeling problems. Your report may include one or more of these violations:

  • label — A form element does not have a corresponding label. This covers <input>, <select>, and <textarea> elements that have no programmatic label.
  • select-name — A <select> element does not have an accessible name.
  • input-button-name — An input button (<input type="submit">, <input type="button">, <input type="reset">) has no discernible text.
  • form-field-multiple-labels — A form field has multiple <label> elements associated with it, which causes inconsistent behavior across screen readers.

Which WCAG Criteria Apply

  • 1.3.1 Info and Relationships (Level A) — Information and relationships conveyed through presentation must also be available programmatically. A visual label next to a text field is a visual relationship; a <label> element makes it programmatic.
  • 3.3.2 Labels or Instructions (Level A) — When content requires user input, labels or instructions must be provided.
  • 4.1.2 Name, Role, Value (Level A) — All user interface components must have a name that can be determined programmatically.
  • 2.4.6 Headings and Labels (Level AA) — Labels must describe the topic or purpose of the associated content or input.

Why Labels Matter

Screen readers announce the label when a field receives focus. Without a label, a screen reader user hears "edit text" or "combo box" with no indication of what information to enter. They cannot independently complete the form.

Speech input users interact by speaking the visible label. A user says "click email" to focus the email field. If there is no programmatic label matching the visible text, the command fails. The user has to resort to numbered overlays or grid navigation, which is slow and frustrating.

Labels expand the click target. A properly associated <label> element makes the label text clickable — clicking it focuses the associated input or toggles the associated checkbox. This helps users with motor impairments who have difficulty clicking small form controls, and it helps everyone on touch devices.

Fix: Input Without a Label

This is the most common form label failure. The fix is to add a <label> element with a for attribute that matches the input's id.

Before — no label:

<input type="text" name="email" id="email" placeholder="Email address">

After — explicit label:

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

Alternatively, wrap the input inside the label element. This creates an implicit association and does not require a for/id pair:

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

Both approaches are valid. The explicit approach (using for and id) is more robust across assistive technologies and allows the label and input to be placed anywhere in the DOM relative to each other.

Fix: Select Without a Label

The same pattern applies to <select> elements.

Before:

<select name="country">
  <option value="">Choose country...</option>
  <option value="dk">Denmark</option>
  <option value="de">Germany</option>
</select>

After:

<label for="country">Country</label>
<select name="country" id="country">
  <option value="">Choose country...</option>
  <option value="dk">Denmark</option>
  <option value="de">Germany</option>
</select>

Note: a first <option> with placeholder text like "Choose country..." is not a label. Screen readers may or may not read it when the select receives focus, depending on the browser. Always provide an actual <label>.

Fix: Textarea Without a Label

Before:

<textarea name="message" placeholder="Your message"></textarea>

After:

<label for="message">Your message</label>
<textarea name="message" id="message"></textarea>

Placeholder Is Not a Label

A common misconception: the placeholder attribute is not a substitute for a label. Placeholders fail as labels for multiple reasons:

  • Placeholders disappear when the user starts typing. Users can no longer see what the field is for. If they need to review their input, the context is gone.
  • Placeholder text typically has insufficient contrast. The default browser styling for placeholder text is light gray, which fails WCAG contrast requirements. If you increase the contrast enough to pass, the placeholder looks like pre-filled content and confuses users.
  • Screen reader support is inconsistent. Some screen readers announce placeholder text, others do not. Some announce it only when the field is empty. It is not a reliable accessible name.
  • Placeholders cause cognitive load. Users with cognitive disabilities may confuse placeholder text with entered data and skip the field.

If your design currently uses only placeholders, add real labels. You can keep the placeholder as a hint, but the label must exist independently:

<label for="phone">Phone number</label>
<input type="tel" name="phone" id="phone" placeholder="e.g. +45 12 34 56 78">

Visually Hidden Labels

Some designs intentionally omit visible labels — for example, a search field with only a magnifying glass icon, or a compact inline form. In these cases, the label must still exist in the DOM but can be visually hidden using CSS so screen readers still announce it.

Before — no label at all:

<input type="search" name="q" placeholder="Search...">

After — visually hidden label:

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

The sr-only class (used by Tailwind, Bootstrap, and many other frameworks) visually hides the label while keeping it accessible. If you do not use a framework, add this CSS:

.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;
}

Do not use display: none or visibility: hidden — both remove the element from the accessibility tree entirely.

aria-label as an Alternative

When a visible label is not possible and you prefer not to add a hidden <label> element, aria-label provides an accessible name directly on the input.

Before:

<input type="search" name="q" placeholder="Search...">

After:

<input type="search" name="q" placeholder="Search..." aria-label="Search">

Use aria-label sparingly. A visible <label> is always preferred because it benefits all users, not just screen reader users. Speech input users also benefit from visible labels they can reference by name.

aria-labelledby for Complex Labeling

When a field's label is composed of multiple elements, or the labeling text already exists elsewhere on the page, use aria-labelledby to reference one or more elements by their id.

Before — label context is split across multiple elements:

<div>Quantity for</div>
<div>Blue T-Shirt (Large)</div>
<input type="number" name="qty" min="0">

After — aria-labelledby combines both text elements:

<div id="qty-prefix">Quantity for</div>
<div id="product-name">Blue T-Shirt (Large)</div>
<input type="number" name="qty" min="0"
       aria-labelledby="qty-prefix product-name">

The screen reader will announce "Quantity for Blue T-Shirt (Large)" when the field receives focus. The aria-labelledby attribute accepts a space-separated list of id values and concatenates their text content in order.

Fix: Button Labels

Buttons need accessible names too. The axe-core input-button-name rule catches buttons without discernible text.

Text content — the simplest approach:

<!-- Before: empty button -->
<button></button>

<!-- After: text inside -->
<button>Submit</button>

Icon buttons — add aria-label:

<!-- Before: icon with no text -->
<button>
  <svg>...close icon...</svg>
</button>

<!-- After: aria-label provides the name -->
<button aria-label="Close">
  <svg aria-hidden="true">...close icon...</svg>
</button>

Input submit buttons — use the value attribute:

<!-- Before: empty value -->
<input type="submit" value="">

<!-- After -->
<input type="submit" value="Send message">

Image input buttons — use the alt attribute:

<!-- Before: no alt -->
<input type="image" src="search-icon.png">

<!-- After -->
<input type="image" src="search-icon.png" alt="Search">

Fix: Fieldset and Legend for Radio Groups and Checkboxes

Individual radio buttons and checkboxes have their own labels, but the group as a whole needs a label too. Without it, a screen reader user hears individual options without knowing the question being asked.

Before — radio buttons with individual labels but no group label:

<label><input type="radio" name="size" value="s"> Small</label>
<label><input type="radio" name="size" value="m"> Medium</label>
<label><input type="radio" name="size" value="l"> Large</label>

After — wrapped in fieldset with legend:

<fieldset>
  <legend>Select a size</legend>
  <label><input type="radio" name="size" value="s"> Small</label>
  <label><input type="radio" name="size" value="m"> Medium</label>
  <label><input type="radio" name="size" value="l"> Large</label>
</fieldset>

The same pattern applies to checkbox groups:

<fieldset>
  <legend>Notification preferences</legend>
  <label><input type="checkbox" name="notify_email"> Email</label>
  <label><input type="checkbox" name="notify_sms"> SMS</label>
  <label><input type="checkbox" name="notify_push"> Push notification</label>
</fieldset>

The <legend> is announced by screen readers before each option in the group. When focusing the first radio button, a screen reader will say "Select a size, Small, radio button, 1 of 3".

Fix: Multiple Labels on the Same Field

The form-field-multiple-labels rule fires when two or more <label> elements reference the same input. This typically happens by accident — a duplicate label in the markup, or a CMS generating an extra label.

Before — two labels referencing the same id:

<label for="name">Full name</label>
<div class="help">
  <label for="name">Enter your first and last name</label>
</div>
<input type="text" id="name" name="name">

After — one label, with help text linked via aria-describedby:

<label for="name">Full name</label>
<div class="help" id="name-help">Enter your first and last name</div>
<input type="text" id="name" name="name" aria-describedby="name-help">

The aria-describedby attribute associates supplementary text with the input. Screen readers announce it after the label, typically with a short pause. This is the correct way to provide extra instructions, help text, or format hints without creating multiple labels.

Required Fields

Indicate required fields both visually and programmatically:

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

Key points:

  • required is the native HTML attribute — browsers enforce it on form submission and screen readers announce "required".
  • aria-required="true" provides the same information to assistive technologies. Using both ensures maximum compatibility.
  • The visual asterisk is hidden from screen readers with aria-hidden="true" because the required state is already communicated programmatically. Without aria-hidden, some screen readers would announce "star" or "asterisk".
  • Include a note at the top of the form explaining that asterisks indicate required fields: "Fields marked with * are required."

Error Messages and Validation

When form validation fails, connect error messages to their fields using aria-describedby and mark invalid fields with aria-invalid.

Before — error message not connected to field:

<label for="email">Email</label>
<input type="email" id="email" name="email">
<div class="error">Please enter a valid email address.</div>

After — error connected and field marked invalid:

<label for="email">Email</label>
<input type="email" id="email" name="email"
       aria-invalid="true"
       aria-describedby="email-error">
<div class="error" id="email-error">Please enter a valid email address.</div>

For dynamic client-side validation, add aria-live="polite" to the error container so screen readers announce errors as they appear:

<div class="error" id="email-error" aria-live="polite"></div>

A complete accessible form validation pattern:

<form novalidate>
  <div>
    <label for="email">
      Email address <span aria-hidden="true">*</span>
    </label>
    <input type="email" id="email" name="email"
           required aria-required="true"
           aria-describedby="email-hint email-error">
    <div id="email-hint">We will use this to send your confirmation.</div>
    <div id="email-error" class="error" aria-live="polite"></div>
  </div>

  <button type="submit">Register</button>
</form>

Note that aria-describedby can reference multiple IDs. In this example, both the hint and the error message are associated with the input, and screen readers will announce both.

Search Forms

Search forms are a special case. A search input inside a <form> with role="search" (or the HTML <search> element) has an implicit purpose, but still needs a label for screen readers.

Before — no label, relying only on the search landmark:

<form role="search">
  <input type="search" name="q" placeholder="Search...">
  <button type="submit">Go</button>
</form>

After — visually hidden label added:

<form role="search">
  <label for="site-search" class="sr-only">Search</label>
  <input type="search" name="q" id="site-search" placeholder="Search...">
  <button type="submit">Search</button>
</form>

Also note the submit button: "Go" is vague. "Search" tells all users what the button does.

In modern HTML, you can also use the <search> element instead of role="search":

<search>
  <form>
    <label for="site-search" class="sr-only">Search</label>
    <input type="search" name="q" id="site-search" placeholder="Search...">
    <button type="submit">Search</button>
  </form>
</search>

CMS-Specific Fixes

WordPress — Contact Form 7: Contact Form 7 generates labels, but the default template often omits them. Wrap each field tag in a [label] shortcode:

<!-- Before -->
Your name
[text* your-name]

<!-- After -->
<label>Your name
  [text* your-name]
</label>

For select fields:

<label>Country
  [select country "Denmark" "Germany" "Sweden"]
</label>

WordPress — Gravity Forms: Gravity Forms generates labels by default. Common issues:

  • Hidden labels: If you set a field's label visibility to "Hidden" in the form editor, Gravity Forms uses class="gfield_label gf_hidden" which uses display: none. This hides the label from screen readers too. Instead, use the "Label Visibility" setting set to "Hidden" which uses a screen-reader-accessible method.
  • Placeholder-only fields: Even with the placeholder add-on, keep labels visible or use the built-in hidden label feature, not CSS display: none.
  • Description placement: Set "Description Placement" to "Below inputs" and Gravity Forms will properly associate descriptions via aria-describedby.

WordPress — WooCommerce Checkout: WooCommerce checkout fields use woocommerce_form_field() which generates labels automatically. Issues arise from:

  • Custom checkout fields added via hooks that skip the label parameter. Always include the 'label' key when adding fields via woocommerce_checkout_fields.
  • CSS hiding labels: Some themes hide WooCommerce labels with display: none for a "clean" look. Replace with the sr-only / screen-reader-text class instead.
  • Optional fields: WooCommerce adds "(optional)" text to labels automatically. Do not remove this — it helps users understand which fields they can skip.
// Adding a custom WooCommerce checkout field with proper label
add_filter('woocommerce_checkout_fields', function($fields) {
    $fields['billing']['billing_vat'] = [
        'type'        => 'text',
        'label'       => 'VAT Number',    // Always include label
        'required'    => false,
        'class'       => ['form-row-wide'],
        'priority'    => 35,
    ];
    return $fields;
});

Testing Your Fixes

Click test: The simplest verification — click the label text. If clicking the label moves focus to the associated input (or toggles a checkbox/radio button), the association is correct. If nothing happens, the for/id pairing is broken or missing.

Browser DevTools: Inspect the input element. In Chrome DevTools, the Accessibility panel (under the Elements tab) shows the element's computed accessible name. If it says "No accessible name" or shows the placeholder text, the label is missing or broken.

Screen reader testing: Tab through your form with a screen reader active. For each field, verify:

  • The label is announced when the field receives focus.
  • Required state is announced ("required").
  • Help text or descriptions are announced after the label.
  • Error messages are announced when validation fails.
  • Radio and checkbox groups announce the group label (legend) before the option label.

Quick screen reader options for testing:

  • macOS: VoiceOver — press Cmd+F5 to toggle on/off. Use Tab to move through form fields.
  • Windows: NVDA (free download) — use Tab to move through fields. Press Insert+T to hear the window title.
  • Chrome: The built-in Accessibility Inspector in DevTools does not replace screen reader testing, but the "Full accessibility tree" view gives a quick overview of what assistive technologies can see.

Automated re-scan: After making your fixes, run another Passiro scan to verify the label, select-name, input-button-name, and form-field-multiple-labels rules all pass. Automated testing catches the structural issues; manual screen reader testing confirms the user experience is correct.

Er nettstedet ditt tilgjengelig?

Skann nettstedet ditt gratis og få WCAG-poengsummen din på noen få minutter.

Skann nettstedet ditt gratis