A control only a mouse can reach
What it costs you
This element is styled and wired as a button but is not one: it has a click handler and no role, no tabIndex and no key handler, so it cannot be reached by Tab, activated by Enter or Space, announced by a screen reader, or driven by voice control. Anyone not using a mouse cannot perform this action at all.
The defect, and the fix
Both samples are scanned as app/components/SortableTable.tsx.
Our test suite runs both through the scanner on every build: the first must be reported, the second must not.
export function SortableTable({ rows, onSort }) {
return (
<table>
<thead>
<tr>
<th className="cursor-pointer select-none" onClick={() => onSort("name")}>
Name
</th>
</tr>
</thead>
</table>
);
}export function SortableTable({ rows, onSort }) {
return (
<table>
<thead>
<tr>
<th>
<button type="button" className="cursor-pointer select-none" onClick={() => onSort("name")}>
Name
</button>
</th>
</tr>
</thead>
</table>
);
}What changed: The handler moves onto a real button, which the browser makes focusable, Enter- and Space-activatable, and announces as a control.
How to fix it
Make it a <button type="button"> — that is the whole fix, and it keeps the styling. If the tag cannot change, add role="button", tabIndex={0} and an onKeyDown that fires the same handler on Enter and Space.
Why this rule doesn't cry wolf
Each clause below exists because it was attacked: someone was asked to find correct code that the rule would flag, and the clause is what stopped it. This is published because a check you cannot audit is a check you have to take on faith.
Native non-focusable tags only (a capitalised component's semantics live in another file); no spread attribute; the handler must do more than stopPropagation/preventDefault; no interactive descendant; not the child of an asChild trigger; never tr/li/tbody; and the element must carry a click affordance (cursor-pointer, select-none, hover:). Each clause removes a class of correct code: prop-forwarding wrappers, click barriers, redundant row affordances, and Radix triggers that merge their attributes into the child at runtime.
This is not a security finding
QA005 is reported in its own section, separately from security findings. It does not change your security grade, and it does not fail your build unless you pass --fail-on-quality. The security catalogue lives at /rules.
Other Accessibility checks
Check your own code
npx xploitscan scan .Runs on every plan, including free. All 12 quality checks.