Linter · Rules
jsx-a11y/label-has-associated-control
Require every <label> to have text and a form control it belongs to
A screen reader announces a form field by reading its label. A <label> that isn't tied to a field is read as loose text, and clicking it doesn't focus anything. A <label> without text leaves the field without a name.
Tie a label to its control with htmlFor, or wrap the control in the label. By default either one is enough.
Configure
Off by default. To turn it on:
[lint.rules]
"jsx-a11y/label-has-associated-control" = "error" # or "warn"
Every option, set to its default:
[lint.rules]
"jsx-a11y/label-has-associated-control" = ["error", {
assert = "either",
depth = 2,
labelComponents = [], # list of strings
labelAttributes = [], # list of strings
controlComponents = [], # list of strings
}]
Examples
Reported:
const form = (
<form>
<label>Email</label>
<input type="email" id="email" />
<label htmlFor="password" />
<input type="password" id="password" />
</form>
);
Not reported:
const form = (
<form>
<label htmlFor="email">Email</label>
<input type="email" id="email" />
<label>
Password
<input type="password" />
</label>
</form>
);
An expression such as {children} could be text or a control, so it counts as both:
const Field = ({ children }) => <label>{children}</label>;
Options
| Option | Default | What it does |
|---|---|---|
assert | "either" | How the label must be tied to its control: "htmlFor", "nesting", "both", or "either". |
depth | 2 | How many levels of children to search for the text and the control. At most 25. |
labelComponents | [] | Components to check as if they were <label>. |
labelAttributes | [] | Attributes that count as the label's text, in addition to alt, aria-label and aria-labelledby. |
controlComponents | [] | Components that count as a control, in addition to input, meter, output, progress, select and textarea. Globs such as "*Field" work. |
With "both", a label that only wraps its control is reported:
const field = (
<label>
Password
<input type="password" />
</label>
);
The rule doesn't find a control that's deeper than depth. This is reported by default and allowed with depth: 3:
const field = (
<label>
Country
<div><span><select /></span></div>
</label>
);
const field = (
<label>
Country
<div><span><select /></span></div>
</label>
);
A component inside a label doesn't count as a control unless you list it:
const field = <label>Email <TextField type="email" /></label>;
const field = <label>Email <TextField type="email" /></label>;
Your own label components are only checked if you list them:
const field = <FieldLabel>Email</FieldLabel>;
An attribute you list in labelAttributes counts as the text:
const field = <label htmlFor="email" text="Email" />;