Require elements with an interactive ARIA role to be focusable.
When an author adds role="button" (or any other interactive widget role) to a <div>, they promise keyboard and screen-reader users that the element behaves like that widget. That promise only holds if the element is reachable by keyboard — either because it is inherently focusable (a real <button>, an anchor with href, a form control, etc.) or because it has a tabindex.
This rule flags elements that carry an interactive ARIA role but have no focus affordance.
All three peer plugins implement the equivalent rule as handler-gated — they only flag <div role="button"> when an interactive event handler (onClick / @click / (click)) is also present:
jsx-a11y/interactive-supports-focusvuejs-accessibility/interactive-supports-focus@angular-eslint/template/interactive-supports-focus
This rule is role-gated — it flags on role alone, regardless of handler presence. Shapes like <div role="button">x</div> with no handler will flag here but not in jsx-a11y / vue-a11y / angular-eslint. That's a deliberate choice: an authored interactive role promises operability irrespective of whether the handler is wired up at the current site (the role is the public contract; the handler is an implementation detail that may move).
If you want peer-parity handler-gated behavior, use template-no-invalid-interactive instead (see also #33), which flags interactive event handlers on non-interactive hosts and honors the role="presentation" / aria-hidden escape hatches.
This rule forbids the following:
<template>
{{! role without tabindex on a non-focusable host }}
<div role="button">Click</div>
<span role="link">Visit</span>
<div role="checkbox" {{on "click" this.toggle}}></div>
{{! anchor / area without href is not inherently focusable }}
<a role="button">x</a>
{{! hidden input loses its focus affordance }}
<input type="hidden" role="button" />
{{! contenteditable="false" explicitly opts out of focus }}
<div role="textbox" contenteditable="false">x</div>
</template>This rule allows the following:
<template>
{{! Inherently focusable hosts }}
<button role="button">x</button>
<a href="/next" role="link">Next</a>
<input role="combobox" />
{{! Any tabindex satisfies the focus requirement }}
<div role="button" tabindex="0"></div>
<div role="menuitem" tabindex="-1"></div>
<div role="button" tabindex={{this.ti}}></div>
{{! contenteditable makes an element focusable }}
<div role="textbox" contenteditable="true">Edit</div>
{{! Dynamic role — conservatively skipped }}
<div role={{this.role}}></div>
{{! Non-widget roles are outside scope }}
<div role="region"></div>
{{! Component invocations — out of scope }}
<MyButton role="button" />
</template>- Interactive ARIA roles are derived from
aria-query: non-abstract roles that descend fromwidget, plustoolbar(matching jsx-a11y's convention). - Component invocations (PascalCase,
@arg,this.x,foo.bar,foo::bar) are skipped — their rendered output is opaque to the linter. - Custom elements not present in aria-query's DOM map are skipped.
- Dynamic role values (
role={{this.role}}) are conservatively skipped. - Related rule:
template-no-invalid-interactivecovers a different concern — it flags interactive event handlers on non-interactive elements. This rule enforces the inverse: when an interactive ARIA role has been declared, the element must also be focusable. The two rules are complementary and can both fire on the same element when appropriate.