CloseTrace

User behavior

Dead click

Definition

A dead click is a click on something that looks interactive — a styled div, an icon, a label, a disabled button — but produces no observable response: no DOM mutation, no navigation, no network request within roughly 1.5 seconds. It is the single clearest signal that an interface is misleading users.

Also called: no-op click, unresponsive click

What technically counts as a dead click?

A click is classified as dead when all of the following are true within a short window (typically 1000 to 1500 ms) after the click event:

  • No MutationObserver callbacks fire on the document
  • No popstate or hashchange event occurs
  • No fetch or XMLHttpRequest request is initiated
  • The clicked target is not a natively interactive element handling its own UI (e.g. <input type="checkbox"> toggling)
  • The target either has no event listener, or its listener silently failed

Some implementations also require the target to "look clickable" — meaning it has cursor: pointer, an onclick attribute, a button-like role, or visual styling that suggests interactivity.

Why does dead click detection matter?

Dead clicks are leading indicators of UX failure because they reveal a mismatch between what the user expected and what the page delivered:

  • They expose silent JavaScript errors that never reach Sentry
  • They surface elements styled like buttons but missing handlers
  • They flag stale React or Vue components that lost their event bindings after a re-render
  • They identify accessibility failures where assistive tech sees nothing clickable
  • They predict rage clicks and abandonment

Unlike error monitoring, dead clicks catch the class of bugs where nothing technically broke — the code just did not do what the user thought it would.

How do you detect a dead click?

The detection pattern uses MutationObserver plus a short timer:

const observer = new MutationObserver(() => {});
observer.observe(document.body, {
  childList: true, subtree: true, attributes: true, characterData: true,
});

document.addEventListener("click", (e) => {
  const target = e.target;
  let mutated = false;
  const mo = new MutationObserver(() => { mutated = true; });
  mo.observe(document.body, { childList: true, subtree: true, attributes: true });

  setTimeout(() => {
    mo.disconnect();
    if (!mutated && looksClickable(target)) {
      report("dead_click", { selector: cssPath(target) });
    }
  }, 1500);
});

In production you also exclude scroll-only changes, ignore hover effects, and require that looksClickable returns true so plain text clicks do not pollute the data.

What are the most common causes?

  • A <div> or <span> styled with cursor: pointer but missing an onClick handler
  • A React component that re-rendered before its handler bound, dropping the listener
  • An event handler that throws synchronously and aborts before mutating state
  • A form submit button outside its <form> element
  • A disabled button without a visual disabled state
  • An anchor with href="#" and a handler that calls preventDefault() then fails
  • An overlay or modal absorbing pointer events without responding

How is a dead click different from a rage click?

AspectDead clickRage click
Required clicks13 or more
Required intervalNone1 to 2 seconds
Core signalThe element does nothingThe user is frustrated
Causal orderComes firstComes after dead clicks

Dead clicks are the cause; rage clicks are often the symptom.

How do you fix dead clicks?

  • Promote pseudo-buttons to real <button> elements with proper type attributes
  • Add disabled plus a visual treatment when an action is unavailable
  • Wrap handlers in try / catch and report exceptions
  • Use event delegation on a stable parent rather than attaching to elements that re-render
  • Verify post-render that the handler is still attached (React StrictMode helps)
  • Add a no-op visual feedback (focus ring, ripple) so the user at least knows the click registered

How it relates to CloseTrace

CloseTrace tracks dead clicks automatically as part of session capture and groups them by stable selector, so a single broken button shows up as one issue with a count and a list of replays — not as a hundred unrelated events to triage.