ESLint is a fantastic tool that makes it easy for developers to follow accepted standards. ESLint plugins enhance our coding environment with subsets of standards. In this post we’ll look at jsx-a11y
and five of the most-used (by Google search volume) WCAG and WAI-ARIA accessibility guidelines it enforces.
This tutorial will help you understand each rule and provide a fix for the errors the linter throws for each rule.
jsx-a11y/control-has-associated-label
This rule requires interactive elements to have a text label. That can take several forms, including the following:
- a
button
element’s text - using an
aria-label
attribute - and an alt attribute in an img tag
If you are using jsx-a11y and ESLint reports the “A control must be associated with a text label” eslint error, it is usually pretty simple to resolve. Here’s an example where my input element doesn’t have any labeling:


It’s fixed simply by adding the aria-label
attribute.
This is a simple html element with a simple fix. What if you have a custom component you created in a library like React? How do you make sure it is reviewed by the rule and how can you fix it if it has custom props or attributes?
Fortunately, jsx-a11y/control-has-associated-label
can be passed an option to inform it of custom components to parse. It can also be given a list of elements not to review.
I created a CustomButton component to test this.
export default function CustomButton(props) {
const text = props.text;
return (
<button>{text}</button>
);
}
And I configured the option accordingly. The controlComponent field tells the rule about additional components to review. The ignoreElements field wasn’t required for this demo, but I can use it to keep ESLint from reviewing certain elements like button
.
"jsx-a11y/control-has-associated-label": [ 2, {
"controlComponents": ["CustomButton"],
"ignoreElements": [
"button"
]
}]
Let’s see it in action:

I still see the warning “A control must be associated with a text label.” This is good because I didn’t configure the option to know which prop I would use to pass in an aria-compliant label.
I need to add one more field to my option: "labelAttributes": ["text"]
Now ESLint knows which prop will be used for passing a label. I can also still use the aria-label
prop or pass text as a child: <CustomButton>Save</CustomButton>
and these will satisfy the control-has-associated-label
rule as usual.
Here’s the docs for the rule if you want to read more.
jsx-a11y/no-static-element-interactions
This rule requires static elements that are also interactive to have a role
attribute. An example of this is as simple as a div with a click handler. Another example is an anchor tag (which is interactive by default).
A surprising amount of interactive elements are static elements with no semantic meaning. Read about the jsx-a11y/no-static-element-interactions rule here.
Here’s an example where a static element with an event handler requires a role. It throws error “static html elements with event handlers require a role”:

This is fixed simply by adding role="button"
. Here’s the final code:
<span tabIndex={-1} role="button" onClick={alertHello} onKeyPress={alertHello}>Hello</span>
The rule can be configured to apply to only certain event handlers. For example, I modified the rule to only require roles when onClick is detected. Below is my .eslintrc.json configuration for no-static-element-interactions:
"jsx-a11y/no-static-element-interactions": [
"error",
{
"handlers": [
"onClick"
]
}
]
jsx-a11y/click-events-have-key-events
This rule requires elements with onClick events to also have at least one of the following events: onKeyUp
, onKeyDown
, onKeyPress
. This is important because users with physical disabilities may be unable to use a mouse or who use a screenreader.
jsx-a11y/click-events-have-key-events will often be triggered with the jsx-a11y/no-static-element-interactions rule because they can both trigger off of elements with onClick listeners.
I’ll use the same element as the no-static-element-interactions example, but this time it is missing the onKeyPress
listener. It throws error “visible, non-interactive elements with click handlers must have at least one keyboard listener”.

This can be fixed simply by adding onKeyPress
(or onKeyUp
/onKeyDown
).
There are no options or arguments for this rule, but it can be set to error
or warn
.
"jsx-a11y/click-events-have-key-events": [
"warn"
]
Read the docs for the rule here.
jsx-a11y/anchor-is-valid
The anchor-is-valid rule has one of the most robust pages of documentation in the plugin, and this is because it has lots of explanation and supporting rules.
The short version is that assistive technologies anticipate that anchor tags will navigate somewhere (whether internally in our HTML page or to another HTML page), and so we should adhere to that expectation in our applications.
Here we see the error “The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value” on an anchor tag without a href
attribute.

Simply adding an href
with a valid address like href="www.smartdevpreneur.com"
will satisfy the rule. Valid internal addresses that start with a hash are also acceptable.
anchor-is-valid
has .eslintrc options in case you have custom components that should be checked. Here’s an example:
"jsx-a11y/anchor-is-valid": [
"error",
{
"components": ["Link"],
"specialLink": ["hrefDefault"],
"aspects": ["noHref"]
}
]
The components
field will check whatever custom components or third-party library components are used. For example, if you use the react-router library you likely want to enforce this rule on any Link components.
The specialLink
field allows for additional props besides href
to satisfy the rule.
The aspects field limits the check to failing for certain situations. Above I specified that it can only fail if the href
is omitted or has empty string value. I could also include invalidHref
, which means the code would fail if the href
didn’t contain a functioning link.
Read the docs for the rules here.
jsx-a11y/label-has-associated-control
While this rule sounds similar to the control-has-associated-label
discussed above, it is actually significantly different.
jsx-a11y/label-has-associate-control
requires any labels to either wrap a control or be associated with a control through the htmlFor
attribute. In the screenshot below, a label is not paired with an input so it throws the error “A form label must be associated with a control”.

A simple fix is to add a child radio input:
<label>
Email Opt-In
<input type="radio" aria-label="name" />
</label>
Similar to other rules, label-has-associated-control
needs to be able to examine custom components. If I wanted to use Material-UI’s TextField component, I might set up my .eslintrc.json like this rule customization:
"jsx-a11y/label-has-associated-control": [ 2, {
"labelComponents": ["Label"],
"labelAttributes": ["label"],
"controlComponents": ["Input"],
"depth": 3
}]
The labelComponents
field contains any custom labels that need to be paired with a control of some type. The labelAttributes
field contains valid props that can be used as the label. The controlComponents
field contains custom control components eligible as a match for label components.
Read more about the rule here.
Resources
Here’s a link to a Gist with the code from above.
Here’s how to configure the no-unused-vars rule.
Here’s how to fix jsx-a11y/interactive-supports-focus error.
It may be useful to ignore or disable rules for a single line when using jsx-a11y.
If you are wondering how I found the five most common eslint-plugin-jsx-a11y
rules by search volume, I did keyword research with Ubersuggest and supplemented it simply by seeing what Google suggested when I typed “jsx-a11y”.