Web Platform · DOM and Events
Pointer Events: Capture, Coalescing, and the Hit-Test Boundary
A pointer stream follows one device from down to up, and capture replaces hit testing as the routing rule while coalescing hides the samples the browser merged into a frame.
A mouse produces one stream of movement because there is one mouse. Touch produces one stream per finger, and a stylus produces a stream that carries pressure and tilt. Pointer Events model all three as independent pointers, each identified by a pointerId that stays constant from pointerdown to pointerup or pointercancel. That identity is the foundation for everything else in the API: a pointer is a session, not an event, and code that tracks a drag tracks a session. Each event still propagates along the path described in event propagation, with the hit-test result as its target.
One stream per pointer
pointerdown begins a stream, pointermove reports motion, and pointerup ends it. pointercancel ends it without completion, which happens when the browser takes the interaction over for a gesture such as scrolling or a system-level swipe. After cancellation, no further move events arrive for that pointer, and no click is generated.
The pointerType string distinguishes mouse, pen, and touch. It is a hint, not a branch point for correctness: the same code path should handle all three, with optional refinements for pressure or width/height on contact pointers. Mouse-derived events (mousedown, mousemove, mouseup, click) still follow pointer events for compatibility, generated from the same input. Ordering matters for handlers that mix the two: pointerdown precedes mousedown, pointerup precedes mouseup, and click follows pointerup.
Every pointer event is targeted by hit testing: the browser finds the topmost element at the pointer’s coordinates in the flattened tree, respecting pointer-events, clipping, and transforms. Hit testing happens per event, which is why a drag that leaves an element stops producing events on it without capture. The target can also change between two moves without any pointer button state changing, and pointerover/pointerout and pointerenter/pointerleave report those transitions in the same pattern as their mouse equivalents.
Capture overrides the hit test
element.setPointerCapture(pointerId) redirects all subsequent events for that pointer to the element, regardless of where the pointer is. releasePointerCapture ends the redirection, and hasPointerCapture reports the current state. lostpointercapture fires when capture ends for any reason, including pointerup, pointercancel, and the element being removed from the document.
Capture is what makes drag implementations simple. Without it, tracking a drag means listening on the document for the whole gesture and converting coordinates, because the pointer can leave the source element. With it, the source element receives every move until the gesture ends, even when the pointer is over an unrelated element or outside the window.
handle.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return; // primary button only
handle.setPointerCapture(event.pointerId);
handle.dataset.dragging = 'true';
});
handle.addEventListener('pointermove', (event) => {
if (handle.dataset.dragging !== 'true') return;
// one event per pointer, even though the pointer may be anywhere
moveTo(event.clientX, event.clientY);
});
handle.addEventListener('lostpointercapture', () => {
delete handle.dataset.dragging;
});
Touch input has implicit capture: on pointerdown over an element, the browser captures the pointer to that element automatically. The finger can drift away and the original element still receives the moves. Mouse input has no implicit capture, so a mouse drag that leaves the element stops delivering moves to it unless setPointerCapture is called. Code that assumes the original element keeps receiving moves works on touch and fails with a mouse unless capture is set explicitly.
Capture applies at the next pointer event, not retroactively to the one that requested it. While capture is active, boundary transitions that would normally fire as the pointer crosses elements are not delivered to other nodes on the path.
The browser merges samples before dispatch
Input hardware reports far more often than the display refreshes. Movement samples that arrive between two frames are merged into a single pointermove for the frame, with the coordinates of the most recent sample. The merge keeps event dispatch bounded by the refresh rate instead of by the device rate, which is why a high-polling-rate mouse does not multiply the handler calls per second.
The merged-away samples are not discarded. getCoalescedEvents() on a pointermove returns the individual events that were combined into it, in order, each with its own coordinates and timestamp. Drawing applications read them to reconstruct a smooth path instead of connecting one frame’s point to the next, which matters when the pointer moves quickly relative to the frame interval.
canvas.addEventListener('pointermove', (event) => {
if (event.getCoalescedEvents === undefined) {
drawSegment(event.clientX, event.clientY);
return;
}
for (const sample of event.getCoalescedEvents()) {
drawSegment(sample.clientX, sample.clientY);
}
});
getPredictedEvents() goes the other direction: it returns estimated future positions based on the current velocity, intended to reduce perceived latency in drawing. The prediction is a guess and can be wrong at direction changes; consumers are expected to redraw from truth on the next real event. pointerrawupdate is the escape hatch for code that wants each raw sample as it arrives rather than per frame, delivered before the corresponding pointermove. It fires at device rate, so a handler on it must be cheap, and it is not cancelable.
touch-action decides whether the pointer survives
On a touch screen, a movement can be either an application gesture or a browser gesture. The touch-action CSS property declares which browser gestures the element permits, and it is consulted at pointerdown time. auto permits everything, none permits nothing, pan-x and pan-y permit one axis, and manipulation permits panning and pinch-zoom but not double-tap zoom.
When the browser decides to handle a gesture, it sends pointercancel and stops dispatching moves. The cancellation is not an error: it is the contract for ceding the interaction. Code that calls preventDefault on pointerdown in an attempt to keep moves will fail on touch: scrolling is not gated on cancellation the way some mouse interactions are, and a listener registered as passive is not permitted to cancel at all, as listener registration and options explains. touch-action is the control surface.
The property is not inherited, but the browser considers the values on the touched element and its ancestors when deciding which gestures to allow, so a value set on a container constrains pointers that start on its descendants. Setting it to none on a large scrollable region removes scrolling entirely for pointers that start there, which is why it belongs on the draggable element rather than on the page.
What pressure and geometry actually mean
pressure, tangentialPressure, width, height, tiltX, tiltY, and twist are reported when the device provides them. For hardware without pressure sensing, pressure is 0.5 while any button or contact is active and 0 otherwise, which is a convention rather than a measurement; the same rule covers touch and pen devices with no sensor. Pressure is normalised to the range 0 to 1, tangentialPressure runs from -1 to 1, and the tilt and twist angles are in degrees.
The geometry fields are in CSS pixels for a contact patch, and they are approximations. A stylus reports a small width and height; a finger reports something related to the contact ellipse. Treating them as exact or as device-independent produces input handling that varies by hardware. pointerId is the only field that is stable enough to key state on.
The compatibility layer and the click that never arrives
Browsers synthesise mouse events from pointer input so that code written before Pointer Events keeps working. The synthesis follows the pointer stream: mousedown after pointerdown, mousemove alongside pointermove, mouseup after pointerup. The click event is dispatched on the nearest common ancestor of the down and up targets when the primary button is released, and its detail counts consecutive clicks at the same position, which is how double and triple clicks are distinguished.
A cancelled pointer breaks the chain. pointercancel suppresses the remaining pointer events and the synthesised mouse events for that interaction, so a drag that the browser converted into a scroll produces no click and no mouseup. Handlers that clean up on mouseup therefore leak state when the browser takes over; cleanup belongs on lostpointercapture, which fires in both cases.
Right-button and middle-button presses arrive with button set to the corresponding index and buttons as a bitmask of the currently held buttons. button describes the change, buttons describes the state, and code that checks one when it means the other misbehaves on chained presses. contextmenu is generated separately from the pointer stream and can be suppressed, though on touch devices a long press may produce one through platform-level handling that preventDefault on pointerdown does not reliably intercept.
Where the pointer stream ends
pointerup ends a stream and releases capture, so a second pointerdown on the same element starts a new one. Identifiers are only required to be unique among pointers that are currently active, so a released identifier can be reused later. Code that stores per-pointer state in a map keyed by pointerId must delete entries on both pointerup and pointercancel, or the map grows with every gesture. Multi-touch handling depends on that bookkeeping: two fingers produce two interleaved streams with distinct ids, and any shared state, such as a pinch origin, must be maintained per id rather than per element.
The stream also ends if the element is removed from the document while captured. Its capture override is set to the document, so the resulting lostpointercapture is dispatched at the document rather than at the detached element, and the remaining events hit-test against whatever is now under the pointer. Cleanup that runs on removal has to assume the gesture was interrupted, not completed.
References
- Pointer Events Level 3 — W3C
- Pointer Events Level 2 — W3C
- Pointer events — MDN Web Docs