Skip to main content
ZELSIS

A div with onClick is not a button

The Pay button looks fine and works with a mouse. A customer who uses the keyboard presses Tab, and focus jumps straight past it. A screen reader reads the label as plain text. Neither of them can pay, and neither of them files a bug report. They leave.

Why it happens

A <button> gets three things from the browser for free: it is in the Tab order, Enter and Space press it, and assistive technology announces it as a button. A <div> or <span> gets none of them. Adding onClick only handles the mouse.

Bad: mouse only
<div className="pay-button" onClick={handlePay}>
  Pay now
</div>

The cost is real. People who cannot use a mouse, people with a broken trackpad and power users who move by keyboard all hit the same wall. In many markets, including the US and the EU under the European Accessibility Act, an online checkout that keyboard users cannot complete is also a legal risk.

The fix: use a real button

Swap the element. Set type="button" so it does not submit a surrounding form by accident, and reset the default styles if they get in the way. Keep a visible focus style so keyboard users can see where they are.

Good: native button
<button type="button" className="pay-button" onClick={handlePay}>
  Pay now
</button>

/* CSS */
.pay-button { all: unset; cursor: pointer; /* your styles */ }
.pay-button:focus-visible { outline: 2px solid #34d399; outline-offset: 2px; }

If the click goes to another page, use a link (<a href> or your framework's Link) instead. Links also work with the keyboard, open in a new tab and show the address on hover.

When you cannot change the element

Sometimes a library renders the wrapper for you. Then add what the browser would have given a button: a role, a place in the Tab order and a key handler for Enter and Space.

Fallback: role, tabIndex and keys
<div
  role="button"
  tabIndex={0}
  onClick={handlePay}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault(); // stop Space from scrolling the page
      handlePay();
    }
  }}
>
  Pay now
</div>

Verify it

Test it: Put the mouse away and go through your main flow (sign up, checkout, settings) with Tab, Shift+Tab, Enter and Space. Every action must be reachable and you must always see where the focus is. In code, the jsx-a11y ESLint rules click-events-have-key-events and no-static-element-interactions catch the same pattern, and an automated check such as Lighthouse or axe reports missing roles.

How Zelsis catches this

Zelsis flags <div> and <span> elements with onClick and no role, tabIndex or key handler, and suggests the <button> fix on the pull request.

  • UI-A11Y-02Clickable div or span without role, tabIndex or keyboard handler

Check your own repo

Public repositories scan free. Paste a GitHub URL on the dashboard and every finding comes with the file, the line and a fix.

Scan a repository free