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>

© 2026 Xygeni. All rights reserved