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.

1 · Mouse events
mousedown / mousemove / mouseup — one pointer, no pressure, no multi-touch
↓ touchscreens arrive, but need multi-touch
2 · Touch events
touchstart / touchmove / touchend — multi-touch, but a separate code path from mouse; pens still awkward
↓ unify everything under one API
3 · Pointer events
pointerdown / pointermove / pointerup — mouse, touch, and pen through a single set of events
Three generations of pointing input. Each layer was added to cover a gap the previous one couldn't.

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, then mouseup, then click, 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 addedtouchstart, 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.

elem.onmousedown
elem.onpointerdown
Renaming is most of the migration: swap the mouse prefix for pointer.

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.
  • isPrimarytrue for 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 always 1.
  • height — height of the contact area. Same fallback of 1 where unsupported.
  • pressure — tip pressure from 0 to 1. On hardware with no pressure sensor, it’s 0.5 while pressed and 0 otherwise.
  • tangentialPressure — normalized tangential (barrel) pressure.
  • tiltX, tiltY, twist — pen-only geometry describing how the stylus is angled and rotated against the surface.
widthheightpressure grows with how hard you press (0 → 1)
Contact geometry as reported for a fingertip touch. A mouse would report width/height of 1 and no meaningful tilt.

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.

interactiveLive pointer inspector — read the event's fields as you press and move

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:

  1. First finger touches: pointerdown fires with isPrimary = true and some pointerId.
  2. Second (and further) fingers touch, while the first stays down: each fires its own pointerdown with isPrimary = false and a different pointerId.
👆 finger 1
pointerId: 2
isPrimary: true
👆 finger 2
pointerId: 3
isPrimary: false
👆 finger 3
pointerId: 4
isPrimary: false
Each finger gets its own pointerId. Only the first one down is primary.

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:

interactivePress the pad — every pointer is logged with its id and isPrimary

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:

pointerdown — user presses the note to start dragging
pointermove — pointer moves, maybe fires several times
pointercancel — native image drag kicks in and steals the gesture
✗ no more pointermove — the browser now owns the drag (it may even drop the image into another app)
Without preventing defaults, the browser's native image drag hijacks the gesture and cuts off your pointermove stream.

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:

interactiveNo defaults prevented — try to drag the note and watch pointercancel

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:

  1. 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.
  2. 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 &#123; touch-action: none &#125;. Now the browser leaves touch interactions on the note alone.

With those two lines in place, the browser stops interfering and stops emitting pointercancel:

interactiveBoth defaults disabled — the note drags cleanly, no 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 that pointerId to elem. After you call it, every pointer event with that id behaves as if it happened on elem, 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.

documentthumb(captured)pointer is hereevents retargeted → thumb
Capture redirects a pointer's events to one element even when the pointer wanders off it.

The capture is released:

  • automatically on pointerup or pointercancel,
  • automatically if elem is 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:

interactiveJust the styled markup — no behavior wired up yet

<p></p>

The behavior, using pointer events:

  1. The user presses the thumbpointerdown fires.
  2. They move the pointer — pointermove fires, and your code slides the thumb to 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 the pointerdown handler.
  • Every pointer event until pointerup/pointercancel is now retargeted to thumb.
  • 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:

interactiveA capturing slider — drag past the hover box and it stays quiet

<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:

  1. Cleaner code. No adding and removing document-wide handlers; the binding tears itself down on pointerup.
  2. 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 via setPointerCapture.
  • lostpointercapture — fires when the capture ends, whether from an explicit releasePointerCapture call or automatically on pointerup/pointercancel.
pointerdown
setPointerCapture()
gotpointercapture
pointermove ×N
retargeted to elem
pointerup
lostpointercapture
The lifecycle of a captured pointer, from press to automatic release.

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 pointerId and isPrimary, 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.