Skip to content

Form fields need one label

  • Rule: axe/form-field-multiple-labels
  • Checks: Form field must not have multiple label elements
  • WCAG: 3.3.2
  • Fix class: mechanical: A fix needs no authored text or visual decision.
  • People who use a screen reader or a braille display, most of them blind.

A second <label> holds a hint or an error message

Section titled “A second <label> holds a hint or an error message”

Keep one <label> for the field’s name. Make the hint or error an ordinary element with an id, and point the field’s aria-describedby at it.

<label for="password">Password</label>
<input type="password" id="password" autocomplete="new-password" aria-describedby="password-hint">
<p id="password-hint">Use at least 12 characters.</p>

One <label> wraps the field and another points at it with for

Section titled “One <label> wraps the field and another points at it with for”

Merge the text into one <label> connected with for and id. Screen readers may announce only one of the two.

Text before and after the field reads as one sentence with it

Section titled “Text before and after the field reads as one sentence with it”

Make the text before the field its <label> and the text after it a description, through aria-describedby.

<label for="nights">Stay for</label>
<select id="nights" aria-describedby="nights-unit">
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<span id="nights-unit">nights</span>

Turning the extra <label> into a <span> with no connection to the field

Section titled “Turning the extra <label> into a <span> with no connection to the field”

axe stops asking, but the hint or error is no longer tied to the field, so screen reader users do not hear it when they reach the field.

The part of the fix that differs in React, in its syntax. The rest of the guidance holds.

A field component renders its own <label>, and its caller wraps it in another

Section titled “A field component renders its own <label>, and its caller wraps it in another”

Let the component own the one label. Take hints and errors as props and point aria-describedby at them, with IDs built from useId, joined to any aria-describedby the caller passes and left out when there is none.

function TextField({ label, hint, id: idProp, 'aria-describedby': describedBy, ...rest }) {
const generatedId = useId();
const id = idProp ?? generatedId;
const hintId = `${id}-hint`;
const describedByIds = [describedBy, hint && hintId].filter(Boolean).join(' ');
return (
<>
<label htmlFor={id}>{label}</label>
<input {...rest} id={id} aria-describedby={describedByIds || undefined} />
{hint && <p id={hintId}>{hint}</p>}
</>
);
}

Spreading the caller’s props after the component’s id and aria-describedby

Section titled “Spreading the caller’s props after the component’s id and aria-describedby”

A caller’s value in the spread replaces the component’s, cutting the label or the hint off from the input. Spread first, then set the attributes the fix depends on.

axe-core’s documentation of the rule

CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.