Interactive Role Without Focus
ID |
html.interactive_role_needs_focus |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Html |
Tags |
accessibility, reliability |
Description
Flags an element that declares an interactive ARIA role and handles activation, yet cannot receive
keyboard focus, because it is neither natively focusable nor given a tabindex.
Rationale
An interactive role plus a handler is a promise that the element behaves like a control, and the first thing a control has to do is take focus. Without it the element is announced as a button to a screen reader user who can never reach it: the Tab key skips it, so does the screen reader’s own control navigation, and the only way to activate it is a pointer. This is the most common way a custom control ends up mouse-only, and it is invisible in testing because the mouse path works perfectly.
<div role="button" onclick="save()">Save</div> <!-- FLAW — mouse only -->
<span role="checkbox" onkeydown="toggle()">Ready</span> <!-- FLAW — same -->
<li role="option" onclick="pick()">One</li> <!-- FLAW — same -->
<div role="button" onclick="save()" tabindex="0">Save</div> <!-- OK -->
<button onclick="save()">Save</button> <!-- OK — focusable already -->
<span role="checkbox" aria-checked="false">Ready</span> <!-- OK — no handler, see below -->
<div role="button" onmouseover="hint()">Save</div> <!-- OK — hover is not activation -->
<ul role="listbox">...</ul> <!-- OK — see below -->
The handler is required, and that is a deliberate narrowing. A role on its own is a claim; a role
with a handler is a claim the code has started keeping. An element with an interactive role but
neither handler nor tabindex is far more often decoration, a placeholder or half-built markup than
a broken control. The handlers that count are onclick, the three key handlers, and mouse down / up;
a hover handler is not one, since it observes the pointer rather than acting on it.
A natively focusable element needs nothing further — a button that also declares role="button" is
redundant but reachable. Everything else has to opt in with a tabindex, and its presence is enough
here: 0, -1 or a value settled at render time, the author has thought about focus.
The roles reported are the ones whose own element is the focus target. A composite widget role
(grid, listbox, menu, tablist, tree) manages focus for its children rather than taking it,
so a container declaring one is correct as written and is not reported; its items are, since they
carry option, menuitem or treeitem.
The judged role has to be plain HTML with a literal value, so th:role, a template expression and
a valueless role are left alone, as is a namespaced element. The two attributes read only for their
presence — the tabindex that exempts, and the handler that triggers — count in either spelling,
since a th:tabindex or a th:onclick does reach the rendered page.
Remediation
Add tabindex="0" so the keyboard reaches the control, and handle the activation keys the role
implies — Enter and Space for a button, Space for a checkbox — since a click handler alone does not
fire for them on a non-native control. Better still, use the HTML element that brings focus, key
handling and state for free.
<button onclick="save()">Save</button>
<div role="button" tabindex="0" onclick="save()" onkeydown="onActivate(event)">Save</div>