Exploring the Five Most-Used eslint-plugin-jsx-a11y Rules

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.

If you need an overview of eslint-plugin-jsx-a11y and how to use it, read this.

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 control-has-associated-label error, it is usually pretty simple to resolve. Here’s an example where my input element doesn’t have any labeling:

jsx-a11y/control-has-associated-label on input element

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.

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:

jsx-a11y/no-static-element-interactions

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"
    ]
  }
]

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:

jsx-a11y/click-events-have-key-events

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.

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.

jsx-a11y/anchor-is-valid

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.

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.

jsx-a11y/label-has-associated-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.

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”.

Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.