TabIndex On A Non-Interactive Element
ID |
javascript.jsx_non_interactive_element_tabindex |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
accessibility, jsx, reliability |
Description
Reports a JSX element put into the tab order although its tag carries a structural role and no interactive role replaces it.
Rationale
A non-negative tabIndex adds a tab stop, and a tab stop is a promise: the user who lands there
expects something to operate. On an element whose role says it is content — a list item, a heading,
a paragraph, an image — there is nothing to operate, so every keyboard user pays an extra Tab press
for a stop that does nothing, and a screen reader announces content in a position where a control
was expected. Long lists make this expensive fast.
<li tabIndex={0}>Details</li> // FLAW - a tab stop that does nothing
<img src="x.png" alt="Chart" tabIndex="0" /> // FLAW - same
<li role="listitem" tabIndex={0}>Details</li> // FLAW - the role confirms it is content
<li role="option" tabIndex={0}>Details</li> // OK - the role makes it a control
<li tabIndex={-1}>Details</li> // OK - focusable from script only
<div tabIndex={0}>Details</div> // OK - div has no role to contradict
<li role={kind} tabIndex={0}>Details</li> // OK - computed role, semantics unknown
Two shapes are commonly meant instead, and neither is reported: tabIndex={-1}, which keeps the
element focusable from script without adding a tab stop, and an interactive role on the same
element, which makes the tab stop honest. div and span are deliberately not judged — they carry
no implicit role for a tab stop to contradict — and neither are the table row and cell tags, which
in a grid take tabIndex exactly as intended.