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
MutationObservercallbacks fire on the document - No
popstateorhashchangeevent occurs - No
fetchorXMLHttpRequestrequest 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 withcursor: pointerbut missing anonClickhandler - 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 callspreventDefault()then fails - An overlay or modal absorbing pointer events without responding
How is a dead click different from a rage click?
| Aspect | Dead click | Rage click |
|---|---|---|
| Required clicks | 1 | 3 or more |
| Required interval | None | 1 to 2 seconds |
| Core signal | The element does nothing | The user is frustrated |
| Causal order | Comes first | Comes 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 propertypeattributes - Add
disabledplus a visual treatment when an action is unavailable - Wrap handlers in
try / catchand 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.