QA005mediumAccessibility

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.

Reported
export function SortableTable({ rows, onSort }) {
  return (
    <table>
      <thead>
        <tr>
          <th className="cursor-pointer select-none" onClick={() => onSort("name")}>
            Name
          </th>
        </tr>
      </thead>
    </table>
  );
}
Not reported
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.