Tab Stop On A Structural Element
ID |
html.non_interactive_element_tabindex |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Html |
Tags |
accessibility, reliability |
Description
Flags an element put into the tab order although its tag carries a structural role and no interactive role replaces it.
Rationale
A tabindex="0" 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.
<p tabindex="0">Read me</p> <!-- FLAW — a tab stop that does nothing -->
<img src="chart.png" alt="Chart" tabindex="0"> <!-- FLAW — same -->
<li tabindex="0">Details</li> <!-- FLAW — same, once per list item -->
<table tabindex="0">...</table> <!-- OK — WCAG 2.1.1 asks for this tab stop -->
<pre tabindex="0">let x = 1;</pre> <!-- OK — same -->
<li role="option" tabindex="0">One</li> <!-- OK — the role makes it a control -->
<p tabindex="-1">Read me</p> <!-- OK — focusable from script only -->
<div tabindex="0">Custom</div> <!-- OK — div has no role to contradict -->
<button tabindex="0">Save</button> <!-- OK — a control belongs in the tab order -->
<p tabindex="${order}">Read me</p> <!-- OK — settled at render time -->
Two shapes are commonly meant instead, and neither is reported. tabindex="-1" keeps the element
focusable from script — the right tool for moving focus to a region after a change — without adding
a tab stop. And an interactive role on the same element (role="option", role="tab") makes the tab
stop honest, which is how a composite widget’s items are built.
Only the value 0 is reported, and that is a deliberate narrowing. A positive tabindex is already
reported by the rule about the focus order, whose own remediation — bring the value back to 0 or
-1 — is the first step here too; reporting both on one attribute would give the author two findings
and two different fixes for a single mistake. A negative value is the correct shape and never a
finding.
div and span are deliberately not judged: they carry no implicit role for a tab stop to
contradict, and a focusable div is the ordinary base for a custom control. The table row and cell
tags are not judged either — in a grid they take a tabindex exactly as intended.
table and pre are not judged, for a different reason: their content overflows, and a keyboard
user who cannot grab a scrollbar reaches it only by focusing the container and using the arrow keys.
WCAG 2.1.1 requires that tab stop and axe-core’s scrollable-region-focusable reports its absence,
so telling the author to remove the tabindex would be telling them to undo an accessibility fix.
The judged tabindex has to be plain HTML with a literal value, so th:tabindex and a template
expression are left alone. A role the element carries exempts it in either spelling, and so does a
role whose value cannot be read here: an unknown role must not be read as the absence of one.
Remediation
Drop the tabindex: content is reached by reading, not by tabbing, and a screen reader already
announces it in place. Where the element has to be focusable from script — a region that takes focus
after an update — use tabindex="-1". Where it really is a control, give it the interactive role and
the key handling that go with the tab stop.
<p>Read me</p>
<div id="results" tabindex="-1">Updated</div>
<li role="option" tabindex="0" onclick="pick()" onkeydown="onKey(event)">One</li>