Skip to content

Form fields need a label

  • Rule: axe/label
  • Checks: Form elements must have labels
  • WCAG: 4.1.2
  • Fix class: content: A fix needs authored text.
  • People who use a screen reader or a braille display, most of them blind.
  • People who control the page by speaking.

Visible text names the field but is not connected to it

Section titled “Visible text names the field but is not connected to it”

Make the text a <label> whose for matches the field’s id exactly, and keep that id unique on the page. Clicking the label then also focuses the field.

<label for="city">City</label>
<input type="text" id="city" autocomplete="address-level2">

Add a visible <label> connected with for and id, in words that say what to enter. Take the words from the field’s purpose in the form; if that is unclear, ask the form’s owner rather than guess.

The <label> is hidden with display: none or visibility: hidden

Section titled “The <label> is hidden with display: none or visibility: hidden”

Show the label. Hide it, with a class that clips it visually but keeps it in the accessibility tree, only where what people see already makes the purpose clear, such as a search box beside a Search button.

<label for="site-search" class="visually-hidden">Search this site</label>
<input type="search" id="site-search">
<button type="submit">Search</button>

axe accepts it as a name, but it disappears as soon as people type, is often faint, and leaves the field without a lasting visible label.

Naming the field only with aria-label where a visible label would fit

Section titled “Naming the field only with aria-label where a visible label would fit”

Screen readers announce it, but sighted people see no label, and voice control users cannot tell what to say to reach the field.

axe’s label rule accepts it, but it appears only on mouse hover, so most people never see it. axe’s best-practice rule label-title-only reports it.

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

A field component hard-codes its input’s id

Section titled “A field component hard-codes its input’s id”

Use the caller’s id if one is passed, or else one from useId, for both the label’s htmlFor and the input’s id, so each instance’s label reaches its own input. Set the id after spreading the other props, so the spread cannot replace it.

function TextField({ label, id: idProp, ...rest }) {
const generatedId = useId();
const id = idProp ?? generatedId;
return (
<>
<label htmlFor={id}>{label}</label>
<input {...rest} id={id} />
</>
);
}

An input component does not pass id on to its <input>

Section titled “An input component does not pass id on to its <input>”

Spread the remaining props onto the <input>, so the id a label points at, and any aria-* props, reach the element.

function Input({ className = '', ...rest }) {
return <input className={`input ${className}`} {...rest} />;
}

Two instances of the form, such as shipping and billing addresses, then share IDs, and both labels point at the first input.

Generating the id with Math.random() or a module-level counter

Section titled “Generating the id with Math.random() or a module-level counter”

The value differs between the server and the client render, so hydration reports a mismatch. useId gives an ID that is unique and the same on both.

axe-core’s documentation of the rule

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