intent-click
v0.1.0
Published
Detect intentional clicks vs accidental taps on touchscreens. Adaptive threshold, ghost click suppression, scroll intent detection.
Maintainers
Readme
intent-click
Detect intentional clicks vs accidental taps on touchscreens. Adaptive threshold, ghost click suppression, scroll intent detection.
Why?
On touchscreens, accidental taps are common:
- User scrolls, finger lands on a button → click fires
- Drag gesture slightly moved → browser still fires click
- iOS Safari ghost clicks 300ms after touchend → hits element underneath
Standard solutions don't work:
e.preventDefault()— breaks scrollingtouch-action: manipulation— doesn't solve drag-vs-clickfastclick— deprecated, only solved 300ms delay
intent-click distinguishes intentional clicks from:
- Drag gestures (movement > threshold)
- Scroll intents (vertical movement > 10px)
- Ghost clicks (same coords within 400ms)
- Long presses (>500ms without movement)
- Concurrent pointers (pinch-zoom started)
Install
npm install intent-clickQuick Start
import { useIntentClick } from 'intent-click/react';
// React
const ref = useIntentClick<HTMLButtonElement>((event) => {
// Guaranteed: intentional click
deleteAccount();
}, { threshold: 'auto' });
<button ref={ref}>Delete Account</button>// Vanilla JS
import { onIntentClick } from 'intent-click/vanilla';
const off = onIntentClick(buttonEl, () => {
deleteAccount();
}, { threshold: 'auto' });Features
- ~800 bytes gzip
- Zero runtime dependencies
- Adaptive threshold: 4px × devicePixelRatio on desktop, 6px × devicePixelRatio on mobile
- Ghost click suppression: 400ms dead zone on same coords ±5px
- Scroll intent detection: vertical movement > 10px → scroll, not click
- Long press discrimination: >500ms without movement →
onLongPress - Concurrent pointer rejection: multi-touch → cancel
- SSR-safe: returns null on server, hydrates in browser
- TypeScript-first: full type inference
- Framework adapters: React, Vue, Svelte, Solid, Angular, vanilla JS
API
interface IntentClickOptions {
// Movement threshold in CSS pixels. 'auto' = adaptive
threshold?: number | 'auto';
// Duration in ms after which it's considered a long press
longPressDelay?: number;
// Callback when long press is detected
onLongPress?: (event: PointerEvent) => void;
// Callback when intent is cancelled
onIntentCancel?: (reason: IntentCancelReason) => void;
}
type IntentCancelReason =
| 'drag' // Movement exceeded threshold
| 'scroll' // Vertical movement > 10px
| 'concurrent-pointer' // Second pointer appeared
| 'pointercancel' // OS intercepted gesture
| 'timeout'; // (reserved)How It Works
- PointerEvent pipeline: single entry point for mouse/touch/pen
- FSM: IDLE → PENDING → MOVING → DECIDED
- Adaptive threshold:
baseThreshold × devicePixelRatio - Ghost click suppression: track last coords, block ±5px for 400ms
- Scroll intent: vertical movement > 10px → cancel
- Long press: >500ms without movement →
onLongPress
Framework Adapters
Docs
Support
See Funding.
License
MIT
