Pointer events
A mouse, a trackpad, a finger on glass, a stylus pressed against a tablet. These are wildly different physical things, yet from your code’s point of view they all do the same job: they point at stuff and press on it. Pointer events give you one API to handle every one of them, without branching your code per device.
If you’ve only ever wired up click and mousemove, this is the upgrade path. Same mental model, more reach.
How we got here
To understand why pointer events exist, it helps to see the two generations of input events that came before them. Each one solved a real problem and then hit a wall.
Walk through it:
-
First there were only mouse events. The whole web was built on them.
When touchscreens went mainstream with phones and tablets, all that existing mouse-based code still needed to work. So touch devices were designed to emit mouse events too, and they still do. Tap a screen and the browser fires
mousedown, thenmouseup, thenclick, faking a mouse so old pages don’t break.That kept things running, but it papers over what a touchscreen can actually do. The biggest gap: you can touch several points at once (multi-touch), and mouse events have no properties to describe more than one contact point. A pinch-to-zoom gesture is invisible to
mousemove. -
So touch events were added —
touchstart,touchend,touchmove— with touch-specific data like a list of active touches. We won’t dig into their details, because pointer events do the same job more cleanly.Even touch events weren’t the full answer. Pens and styluses have their own traits (tilt, pressure, a barrel button), and if you wanted an interaction to work everywhere, you ended up writing two parallel handlers: one for mouse, one for touch. Tedious and bug-prone.
-
Pointer events unify all of it. One set of events covers mouse, touch, and pen. You write the handler once.
Pointer Events Level 2 is supported across every major browser. Level 3 builds on top of it and stays backward compatible.
The event types
Pointer events are named to mirror mouse events, which makes migration mostly a find-and-replace exercise.
| Pointer event | Matching mouse event |
|---|---|
pointerdown |
mousedown |
pointerup |
mouseup |
pointermove |
mousemove |
pointerover |
mouseover |
pointerout |
mouseout |
pointerenter |
mouseenter |
pointerleave |
mouseleave |
pointercancel |
— |
gotpointercapture |
— |
lostpointercapture |
— |
Every mouse<event> has a pointer<event> twin that behaves the same way. The last three have no mouse equivalent — pointercancel and the two capture events are new capabilities that only make sense in the pointer model. We’ll get to each.
Event properties
A pointer event carries everything a mouse event does — clientX, clientY, target, button, and the rest — plus extra fields that describe which pointer fired and what kind it is.
pointerId— a unique id for the pointer that caused the event, assigned by the browser. This is the key to telling multiple simultaneous touches apart.pointerType— the kind of device, as a string:"mouse","pen", or"touch". Use it to branch behavior when a device genuinely needs different treatment.isPrimary—truefor the primary pointer, which is the first finger down in a multi-touch sequence (or the only mouse).
Some devices also report the physical shape and force of the contact:
width— width of the contact area, e.g. how wide your fingertip’s footprint is. On devices that can’t measure it (a mouse), it’s always1.height— height of the contact area. Same fallback of1where unsupported.pressure— tip pressure from0to1. On hardware with no pressure sensor, it’s0.5while pressed and0otherwise.tangentialPressure— normalized tangential (barrel) pressure.tiltX,tiltY,twist— pen-only geometry describing how the stylus is angled and rotated against the surface.
Most hardware doesn’t report these last few, so they see little use in practice. The full list lives in the Pointer Events specification if you ever need it.
Press and move over the pad below to read these fields live. A mouse reports pointerType: "mouse" with pressure snapping between 0 and 0.5; a pen or a pressure-sensitive touchscreen reports real values.
Multi-touch
This is the capability mouse events fundamentally can’t offer: several contact points on screen at the same time. Pinch, rotate, two-thumb games — all of it depends on tracking more than one pointer.
Pointer events handle it through pointerId and isPrimary. Here’s the sequence when someone presses one finger down and then a second:
- First finger touches:
pointerdownfires withisPrimary = trueand somepointerId. - Second (and further) fingers touch, while the first stays down: each fires its own
pointerdownwithisPrimary = falseand a differentpointerId.
The important detail: pointerId belongs to each touching finger, not to the device as a whole. Touch the screen with five fingers at once and you get five separate pointerdown events, each with its own coordinates and its own id. The events from the first finger always carry isPrimary = true.
Once you know a finger’s pointerId, you can follow it. As that finger slides and lifts, its pointermove and pointerup events reuse the same pointerId you saw in its pointerdown. A common pattern is to keep a map from pointerId to state, add an entry on pointerdown, update it on pointermove, and delete it on pointerup.
Here’s a demo that logs pointerdown and pointerup:
You need an actual touchscreen — a phone or tablet — to see distinct values here. On a single-pointer device like a mouse, every event reports the same pointerId with isPrimary = true, because there’s only ever one pointer.
The pointercancel event
pointercancel fires when a pointer interaction is already underway and then gets aborted, so no further events for that pointer will arrive. It’s the browser telling you “I’m taking this over, stop expecting pointermove.”
Common triggers:
- The pointer hardware was physically disabled.
- The device orientation changed (you rotated the tablet).
- The browser decided the gesture is really something it should handle — a native drag, a pan, or a pinch-zoom.
That third one is where people get burned, so let’s see it in action.
Say you’re building drag-and-drop for a sticky note, like in Drag’n’Drop with mouse events. The intended flow:
So the problem is a hijack: pointercancel arrives right as the “drag” begins, and your pointermove handler goes silent. Your custom drag logic never gets to run.
Here’s the drag-and-drop demo, logging only up/down, move, and cancel into the textarea so you can watch the cancel happen:
We want to run our own drag-and-drop, so we have to tell the browser to keep its hands off.
Prevent the default browser action, and pointercancel won’t fire. There are two separate defaults to disable, one for mouse and one for touch:
- Kill native image drag (the mouse-side default). Set
note.ondragstart = () => false, exactly as in Drag’n’Drop with mouse events. This stops the browser from starting its own image drag. - Kill touch gestures (the touch-side default). Touchscreens have other browser actions such as scroll and zoom that can grab the pointer. Turn them off for this element in CSS with
#note { touch-action: none }. Now the browser leaves touch interactions on the note alone.
With those two lines in place, the browser stops interfering and stops emitting pointercancel:
No more pointercancel. From here you can add the code that actually moves the note, and the same drag-and-drop works with a mouse and with a finger.
Pointer capturing
This is a feature unique to pointer events — nothing like it exists for other event types, which is why it feels strange the first time you meet it. The idea itself is small.
The core method:
elem.setPointerCapture(pointerId)— routes all future events for thatpointerIdtoelem. After you call it, every pointer event with that id behaves as if it happened onelem, no matter where on the page the pointer actually is.
Put plainly: it re-targets a pointer’s events to one element until you’re done.
The capture is released:
- automatically on
pointeruporpointercancel, - automatically if
elemis removed from the document, - manually when you call
elem.releasePointerCapture(pointerId).
So why would you want this? The payoff shows up in drag interactions.
Pointer capturing simplifies drag-and-drop-style code. Take a custom slider, like the one from Drag’n’Drop with mouse events. The markup is a strip with a draggable runner (the thumb) inside:
<div class="slider">
<div class="thumb"></div>
</div>
With some styling it looks like this:
<p></p>
The behavior, using pointer events:
- The user presses the
thumb—pointerdownfires. - They move the pointer —
pointermovefires, and your code slides thethumbto follow.- As they drag, the pointer will drift off the thumb, above or below it. The thumb should still track it horizontally and stay glued to the pointer’s x-position.
In the classic mouse-based version, tracking movement even after the pointer leaves the thumb meant attaching mousemove to the whole document. That works, but it’s messy. The nastiest side effect: while the pointer roams the page, it triggers handlers on other elements — mouseover tooltips, hover menus, unrelated UI — none of which should react to a drag in progress.
This is exactly what setPointerCapture cleans up.
- Call
thumb.setPointerCapture(event.pointerId)inside thepointerdownhandler. - Every pointer event until
pointerup/pointercancelis now retargeted tothumb. - On
pointerup, the capture releases itself — no cleanup needed.
Even as the pointer sweeps across the whole document, the handlers fire on thumb. Coordinate fields like clientX and clientY stay accurate the entire time; capture only changes target and currentTarget, never the position data.
The essential code:
thumb.onpointerdown = function(event) {
// retarget all this pointer's events (until pointerup) to thumb
thumb.setPointerCapture(event.pointerId);
// start following the pointer
thumb.onpointermove = function(event) {
// all pointer events are retargeted to thumb, so we can listen right here
let newLeft = event.clientX - slider.getBoundingClientRect().left;
thumb.style.left = newLeft + 'px';
};
// stop following when the pointer is released
thumb.onpointerup = function(event) {
thumb.onpointermove = null;
thumb.onpointerup = null;
// ...run any "drag end" logic here if needed
};
};
// note: no releasePointerCapture call needed —
// pointerup releases the capture automatically
The full demo:
<p></p>
The demo also includes a separate element with an onmouseover handler that shows the current date. Try dragging the thumb across it: the hover handler does not fire, because those events are captured by the thumb. The drag is free of side effects.
Two concrete wins from pointer capturing:
- Cleaner code. No adding and removing document-wide handlers; the binding tears itself down on
pointerup. - No collateral damage. Other pointer handlers on the page won’t be tripped by the pointer while a drag is running.
Capture events
For completeness, capturing itself fires two events:
gotpointercapture— fires when an element starts capturing viasetPointerCapture.lostpointercapture— fires when the capture ends, whether from an explicitreleasePointerCapturecall or automatically onpointerup/pointercancel.
setPointerCapture()
retargeted to elem
Summary
Pointer events handle mouse, touch, and pen input through a single set of handlers.
They extend mouse events: rename mouse<event> to pointer<event> and your code keeps working with a mouse while gaining much better touch and pen support.
For drag-and-drop and richer touch interactions the browser might try to take over, disable the defaults — ondragstart = () => false for the mouse-side native drag, and touch-action: none in CSS for the touch-side gestures — on the elements you’re driving. That keeps pointercancel from cutting you off.
Beyond parity with mouse events, pointer events add:
- Multi-touch via
pointerIdandisPrimary, so you can track several fingers independently. - Device data such as
pressure,width/height, and pen tilt. - Pointer capturing to retarget a pointer’s events to one element until
pointerup/pointercancel, which makes drag interactions cleaner and side-effect free.
They’re supported in every major browser, so switching over is safe — especially when IE10 and Safari 12 aren’t on your list. Even for those, polyfills exist to fill the gap.