A card in a web page registers handlers for both touchstart and mousedown; on a phone the action runs twice. Why does that happen, and how do Pointer Events avoid the problem?
answer
- one tap, two event families
- legacy pages needed mouse events
- synthesised after the touch ends
- one stream tagged by device
- pointerType, pointerId, isPrimary
basics
~20 sTouchscreen browsers fire the touch sequence and then replay it as compatibility mouse events so older mouse-only pages keep working, so both handlers run. Pointer Events replace both families with one stream — pointerdown, pointermove, pointerup — tagged by pointerType.
solid answer
~50 sOn a touchscreen, the browser dispatches `touchstart`/`touchend` for the gesture and then, for backwards compatibility with pages written before touch existed, synthesises a mouse sequence — `mousemove`, `mousedown`, `mouseup`, `click` — for the same tap. A page that listens to both families therefore handles one tap twice. The historical patches were ugly: call `preventDefault()` on `touchstart` to suppress the compatibility events, or set a "just handled a touch" flag and ignore mouse events for a few hundred milliseconds. Pointer Events fix it at the source. `pointerdown`, `pointermove`, `pointerup` and `pointercancel` are dispatched for mouse, touch and pen alike, so you register one set of handlers; `event.pointerType` tells you which device it was (`"mouse"`, `"touch"`, `"pen"`) when you genuinely need to branch, and `event.pointerId` plus `event.isPrimary` let you keep multiple simultaneous touches apart. For simple activation, plain `click` remains the best choice — it fires once for touch, mouse and keyboard.
go deeper
Recall that a tap on a phone produces both touch events and synthesised mouse events, so listening to both runs your code twice. Knowing that pointer events give one unified stream is enough here.
Explain the ordering — the compatibility mouse batch arrives after the touch sequence — and describe what preventDefault on touchstart suppresses and why pointerType, pointerId and isPrimary remove the need for that trick.
Show the judgment of choosing per interaction: click for activation, pointer events for continuous gestures, and always handling pointercancel so a browser-owned scroll cannot leave the widget stuck mid-drag.
Own the input strategy across a component library: one supported event family, an explicit multi-touch and stylus policy, and a plan for hybrid touch-plus-mouse devices where capability sniffing at load time is the wrong model.
## Why the browser sends a tap twice Touchscreen browsers arrived into a web made entirely of mouse handlers. If a phone had dispatched only touch events, every existing page would have been dead on arrival. So touch browsers do two things for one finger tap: they dispatch the real touch sequence, and then they *synthesise* a mouse sequence describing the same interaction. The compatibility batch — typically `mousemove`, `mousedown`, `mouseup`, then `click` — is dispatched after the touch sequence ends, not interleaved with it, which is why the second run of your handler feels like a delayed echo. A page that registers `touchstart` **and** `mousedown` for the same behaviour therefore runs it twice on a phone and once on a desktop. Symptoms in the wild: a counter incrementing by two, a modal that opens and immediately re-opens, an analytics event double-counted only on mobile. ## The old workarounds Two patterns dominated before Pointer Events shipped broadly. **Cancel the touch default.** Per the Touch Events specification, calling `preventDefault()` on `touchstart` instructs the browser not to dispatch the compatibility mouse events (and with them the synthesised click) for that gesture: ```js el.addEventListener('touchstart', (e) => { e.preventDefault(); // suppresses the compatibility mouse events activate(); }, { passive: false }); ``` It works, but it is a blunt instrument: it also cancels the browser's own default handling of the gesture, and it requires an explicitly non-passive listener because a cancelling handler forces the browser to wait for you before it can scroll. **Guard with a flag.** Set a boolean or timestamp in the touch handler and have the mouse handler bail out if a touch happened recently. This is the "ghost click" folklore, and it is fragile: the window is device-dependent and it breaks on hybrid devices where a user really does alternate finger and mouse. ## What Pointer Events actually change The Pointer Events model treats mouse, single-touch, multi-touch and stylus as instances of one abstraction, a *pointer*. One set of events covers them all: - `pointerdown`, `pointermove`, `pointerup` - `pointercancel` — the browser took the gesture over (it started scrolling or zooming), and **no `pointerup` will follow** - `pointerover` / `pointerout`, `pointerenter` / `pointerleave` - `gotpointercapture` / `lostpointercapture` `PointerEvent` extends `MouseEvent`, so everything you already read off a mouse event — `clientX`, `button`, `buttons`, `shiftKey` — is still there, plus the pointer-specific properties: - `pointerType` — `"mouse"`, `"touch"` or `"pen"`. Branch on this instead of registering two families. - `pointerId` — a stable id per active pointer; essential for multi-touch, where several pointers are live at once. - `isPrimary` — true for the first/primary pointer of each type, letting a single-pointer widget ignore extra fingers with one check. - `pressure`, `width`, `height`, `tiltX`, `tiltY` — richer stylus and contact information that mouse events never had. With one handler set, the double-fire cannot happen, because you never registered the second family: ```js el.addEventListener('pointerdown', (e) => { if (!e.isPrimary) return; // ignore extra fingers if (e.pointerType === 'pen') { /* … */ } activate(); }); ``` ## Compatibility mouse events still exist Adopting Pointer Events does not switch the compatibility mouse events off; the browser still emits them for touch so untouched legacy code keeps working. The discipline is simply: **pick one family per interaction**. Mixing `pointerdown` with `mousedown` for the same behaviour reintroduces exactly the bug you were fixing. ## When plain click is still the right answer For "the user activated this control", `click` beats all of the above. It fires once per activation on mouse, on touch, and on keyboard activation of a focusable control, which means you get keyboard support for free rather than reimplementing it. Reach for pointer events when you need the *stream* — drag, draw, resize, custom gestures — not when you need a tap. ## Two facts that catch people out - **`pointercancel` is not optional.** When the browser decides an in-progress gesture is a scroll, it cancels the pointer. A drag implementation that only resets state in `pointerup` leaves the widget stuck mid-drag. - **Hover is not universal.** `pointerenter` fires for a pen hovering above the digitiser and for a mouse, but touch has no hover state, so hover-only affordances remain unreachable by finger regardless of which event family you use.
- If a page uses only pointer events, do the compatibility mouse events stop being dispatched?No. The browser still synthesises them for touch so that pages written before pointer events keep working; you simply have no listeners registered for them. That is why the rule is to pick one family per interaction — adding a mousedown handler alongside pointerdown for the same behaviour brings the double-fire straight back.
- When would you still prefer click over pointerdown for a button?Almost always, for activation. `click` fires once for mouse, touch and keyboard activation of a focusable control, so keyboard support comes for free and you cannot double-handle. Pointer events earn their place when you need the continuous stream — drag, draw, resize, pinch — where a single activation event tells you nothing.
- What does event.isPrimary let you do that pointerId alone does not?It identifies, without bookkeeping, the one pointer of each type that should drive single-pointer UI. With several fingers down you get several pointerIds and would otherwise have to remember which arrived first; `if (!event.isPrimary) return;` expresses "ignore the extra fingers" in one line while multi-touch code uses pointerId to track each contact separately.
saying these in an interview costs you the question
- Thinks touch devices fire only touch events
- Believes pointer events remove the compatibility mouse events
- Debounces mouse handlers with a timer to suppress ghost clicks
- Uses pointerdown for plain activation and loses keyboard support
- Assumes one pointerdown always has a matching pointerup