TL;DR / Pointer Events provide a unified input model that handles mouse, touch, and pen/stylus interactions through a single set of events, exposing device-specific properties like pressure, tilt, and contact geometry.
How It Works
┌──────────┐
│ Mouse │──────┐
└──────────┘ │
│ ┌──────────────┐ ┌─────────────┐ pointerId
│ │ │ │ pointerdown │ pointerType
│ │ │ ┌────→│ pointermove │ pressure
│ │ Pointer │ │ │ pointerup │ tiltX / tiltY
┌──────────┐ ┌┌─────→│ Events API │────┘ └─────────────┘ width / height
│ Touch │──────┘│ │ │
└──────────┘ │ │ │
│ └──────────────┘
│
┌────────────┐ │
│ Pen/Stylus │─────┘
└────────────┘
Pointer Events unify the historically fragmented input model where developers needed separate mousedown/touchstart/MSPointerDown handlers for different devices. A single pointerdown handler fires for mouse clicks, finger taps, and stylus contact, with the pointerType property ("mouse", "touch", "pen") distinguishing the source when device-specific behavior is needed.
The event lifecycle mirrors mouse events: pointerover -> pointerenter -> pointerdown -> pointermove -> pointerup -> pointerout -> pointerleave. Additionally, pointercancel fires when the browser takes over the pointer (scroll gesture recognition, browser UI interaction), and gotpointercapture/lostpointercapture track capture state changes.
Each PointerEvent extends MouseEvent, adding device-specific properties. pointerId uniquely identifies the pointer across its lifetime (down through up). Multi-touch gives each finger a different ID. pointerType is "mouse", "touch", or "pen". pressure ranges from 0.0 to 1.0 (mouse always reports 0.5 when a button is pressed, 0 otherwise; touch and pen provide true analog pressure). tiltX/tiltY report pen angle in degrees (-90 to 90). width/height report the contact geometry -- a fingertip has width and height proportional to the contact area, while a mouse is always 1x1. twist reports pen rotation around its own axis.
Pointer capture via element.setPointerCapture(pointerId) redirects all events for that pointer to the capturing element, regardless of where the pointer moves. This is essential for drag operations: capture the pointer on pointerdown, handle pointermove on the capturing element (guaranteed delivery even if the pointer leaves the element), and release on pointerup. Without capture, pointermove events stop when the pointer leaves the element's bounds, breaking the drag. releasePointerCapture() is called automatically on pointerup for the primary pointer.
The touch-action CSS property controls which touch gestures the browser handles natively versus passing to your JavaScript. touch-action: none disables all browser touch handling (pan, zoom), giving full control to your pointer event handlers. touch-action: pan-y allows vertical scrolling but passes horizontal gestures to JavaScript. This is critical for custom gesture implementations -- without the correct touch-action, the browser will fire pointercancel when it recognizes a native gesture, aborting your handler.
Compatibility events: browsers fire mouse events after pointer events for compatibility. The sequence for a tap is: pointerdown -> pointerup -> mousemove -> mousedown -> mouseup -> click. Call event.preventDefault() on pointerdown to suppress the subsequent mouse events (but this also suppresses click, which may break accessibility). For most applications, migrating to pointer events means removing mouse and touch handlers entirely, not mixing them.
Multi-touch is handled naturally. Each touch point generates its own pointerdown with a unique pointerId. You track multiple pointers simultaneously by storing active pointer IDs. A pinch-to-zoom gesture produces two pointerdown events, simultaneous pointermove events with both IDs, and two pointerup events. Calculate the distance between the two pointers over time to detect zoom gestures.
The coalesced events API (via event.getCoalescedEvents()) provides all intermediate pointermove positions that the browser coalesced into a single event for performance. This is essential for drawing applications: without coalesced events, fast strokes appear as straight line segments between widely spaced points. Iterating getCoalescedEvents() in a pointermove handler provides sub-frame position accuracy.
predicted events (via event.getPredictedEvents()) extrapolate future pointer positions based on current trajectory. Drawing applications use these to reduce perceived latency by rendering predicted positions ahead of actual input, then correcting as real positions arrive.
The PointerEvent constructor enables synthetic event dispatch for testing. Create a new PointerEvent('pointerdown', { pointerId: 1, pointerType: 'touch', pressure: 0.5, clientX: 100, clientY: 200 }) and dispatch it on an element for programmatic testing of gesture handlers.
Gotchas
touch-action: nonemust be set to preventpointercancel-- without it, the browser firespointercancelwhen it detects a native gesture (scroll, zoom), aborting your custom pointer handling mid-gestureclickevents are suppressed whenpreventDefault()is called onpointerdown-- this breaks keyboard activation and accessibility; use pointer events for tracking but preserve the click path for activation- Hover states do not work correctly on touch devices --
pointerover/pointerenterfire on tap (beforepointerdown), creating sticky hover states; usepointerTypechecks or@media (hover: hover)to gate hover behavior - Mouse events still fire after pointer events -- removing mouse handlers is safe, but libraries and third-party code may still attach them; calling
preventDefault()onpointerdownsuppresses compatibility mouse events but has side effects pointerIdvalues are not stable across sessions or interactions -- a finger that lifts and re-touches gets a newpointerId; do not use it as a persistent identifier for the physical finger