Click confidence measurement from motor behavior. 2KB, zero dependencies, vendor-agnostic.
A mouse click is two events: mousedown (commitment) and mouseup (release). The latency between them — typically 50-140ms — reflects the motor system's confidence at the moment of action. ClickSense captures this hold duration alongside the mouse's approach trajectory leading up to the click.
Hold duration alone is a small signal (~7ms task effect across studies). The real leverage is in combining it with pre-click motor behavior: how the cursor approached the target, whether it decelerated smoothly or corrected course, and how long it paused before committing. Pre-click dwell discriminates correct from incorrect selections by 91ms; hold duration doesn't. But they measure adjacent phases of the same decision — evaluation then commitment — and the combination may access something neither captures alone.
What makes hold duration interesting is what it's independent from. Self-reported cognitive load (NASA-TLX) doesn't correlate with it. Neither does task correctness. It's not a noisy proxy for things we already measure. The strongest effects show up around identity-relevant content and domain expertise: political experts differentiate facts from opinions in their click latency; non-experts don't. Non-voters hold 19ms longer on prospective voting questions than retrospective ones.
Reference: Edmonds (2016) "Learning from Complex Online Behavior", Bloomreach "Big Brains" talk. Data: Edmonds (CrowdFlower, 2015, n=291), Azzopardi & Edmonds (Prolific, 2022, n=227).
npm install clicksenseOr load via script tag:
<script src="dist/clicksense.js"></script>import { ClickSense } from 'clicksense';
const cs = new ClickSense({
enableApproachDynamics: true,
onCapture: (event) => {
console.log(`${event.duration_ms}ms on ${event.target.tag}`, event);
},
});Each captured click produces:
{
duration_ms: 87, // mousedown→mouseup latency
timestamp: 1709312400000,
x: 512, y: 340,
drag_distance: 1,
target: {
tag: 'button',
id: 'submit-btn',
label: 'checkout', // from data-clicksense="checkout"
text: 'Complete Purchase',
},
approach: { // present when enableApproachDynamics: true
approach_velocity_mean: 0.412,
approach_velocity_final: 0.089,
approach_deceleration: -0.001234,
approach_corrections: 3,
approach_distance: 287,
approach_pause_ms: 42,
}
}The pre-click mouse trajectory is where the stronger signal lives. When enableApproachDynamics is enabled, ClickSense maintains a ring buffer of velocity samples from mousemove events (using getCoalescedEvents() for sub-frame resolution on high-polling-rate mice) and harvests summary statistics at the moment of mousedown:
| Field | Description |
|---|---|
approach_velocity_mean |
Mean cursor speed over last 500ms (px/ms) |
approach_velocity_final |
Speed at the most recent sample before click |
approach_deceleration |
Linear regression slope of velocity over last 300ms. Negative = smooth Fitts's law approach; positive = overshooting |
approach_corrections |
Velocity direction reversals — jittery approach vs. smooth ballistic arc |
approach_distance |
Total cursor distance traveled (px) |
approach_pause_ms |
Time since last significant movement. 0 = clicked while moving; 100+ = paused to aim |
A confident click looks like: smooth deceleration, low corrections, short pause, ballistic hold. An uncertain click: course corrections, longer pause, extended hold. The combination captures the full motor signature of a decision.
new ClickSense({
enableApproachDynamics: true, // recommended — pre-click velocity profiling
minDuration: 10, // ms — below = programmatic click
maxDuration: 3000, // ms — above = press-and-hold
maxDragDistance: 10, // px — above = drag, not click
captureText: true, // include truncated innerText
textMaxLength: 80,
buttons: [0], // 0=left, 1=middle, 2=right
scope: null, // CSS selector to limit tracking
onCapture: (event) => {}, // required
});import { ClickSense } from 'clicksense';
import { createPostHogAdapter } from 'clicksense/adapters/posthog';
new ClickSense({
onCapture: createPostHogAdapter(), // uses window.posthog
// or: createPostHogAdapter(posthogInstance) // explicit instance
});
// Sends 'click_confidence' events with flattened propertiesFor sendBeacon, Adobe Analytics, custom endpoints, or localStorage:
import { ClickSense } from 'clicksense';
import { createBufferedAdapter } from 'clicksense/adapters/callback';
new ClickSense({
onCapture: createBufferedAdapter({
flushInterval: 10000, // flush every 10s (default: 30s)
maxBuffer: 200, // flush at 200 events
onFlush: (events) => {
navigator.sendBeacon('/analytics', JSON.stringify(events));
},
}),
});Flushes automatically on visibilitychange (page hide/tab switch).
ClickSense walks the DOM from the click target to find the nearest meaningful element (a, button, [role="button"], input, select, label, or [data-clicksense]). Use the data-clicksense attribute for explicit labeling:
<div data-clicksense="hero-cta" class="promo-card">
<h2>Spring Sale</h2>
<button>Shop Now</button>
</div>- Capture phase listeners — events are seen before
stopPropagation performance.now()— sub-millisecond precision, notDate.now()- Drag filter — displacement > 10px between down/up = drag, discarded
- Touch support —
touchstart/touchendfor mobile - No runtime dependencies — core is pure JS, adapters are optional
cs.destroy(); // removes all listenersnpm run build # → dist/clicksense.js (IIFE), .esm.js, .cjs.js
npm run dev # watch modeSee docs/clicksense-paper.md for the full empirical framing: study designs, effect sizes, confound analysis, and where click duration does and doesn't work.
MIT