Mouse, finger, and pen are three different ways to point at a screen, and for years each needed its own events. Pointer events unify them: one set of events that works the same for all three, with a property that tells you which one it is. This lesson covers the events, what is inside them, how to follow a drag, and how to draw.
| Event | Fires when |
|---|---|
"pointerdown" | a button was pressed or a finger or pen touched |
"pointermove" | the pointer moved |
"pointerup" | the button, finger, or pen was released |
"pointercancel" | the browser took over (for example, the finger became a scroll) |
"pointerenter" / "pointerleave" | the pointer entered or left an element |
"pointerover" / "pointerout" | like enter and leave, but they also fire for child elements |
You listen with addEventListener as with any event. The old mousedown events only react to a mouse, and touchstart only to fingers. A pointer event reacts to all of them.
| Property | Holds |
|---|---|
event.pointerType | "mouse", "touch", or "pen" |
event.pointerId | a number identifying this pointer (each finger has its own) |
event.isPrimary | whether this is the main pointer of its type |
event.clientX, event.clientY | position in the viewport, in pixels |
event.pressure | how hard, from 0 to 1 (0.5 for a mouse button held down) |
event.button, event.buttons | which mouse button changed, and which are down |
You can build and send a pointer event yourself with new PointerEvent(...) and dispatchEvent. That makes a safe way to see exactly what a listener receives, with no mouse needed:
const box = document.createElement("div");
document.body.appendChild(box);
box.addEventListener("pointerdown", function (event) {
console.log(event.type, event.pointerType, event.clientX, event.clientY);
});
box.dispatchEvent(new PointerEvent("pointerdown", { pointerType: "touch", clientX: 10, clientY: 20 }));
box.dispatchEvent(new PointerEvent("pointerdown", { pointerType: "pen", clientX: 30, clientY: 40 }));
This box follows wherever you move the pointer over the area. The same code works with a mouse, a finger, or a pen:
<div id="area" style="height:160px; border:1px solid gray; position:relative; touch-action:none;">
<div id="dot" style="width:20px; height:20px; border-radius:50%; background:tomato; position:absolute; left:0; top:0;"></div>
<p id="info" style="margin:6px;">Move a pointer over this box</p>
</div>
<script>
const area = document.getElementById("area");
const dot = document.getElementById("dot");
const info = document.getElementById("info");
area.addEventListener("pointermove", function (event) {
const box = area.getBoundingClientRect();
dot.style.left = (event.clientX - box.left - 10) + "px";
dot.style.top = (event.clientY - box.top - 10) + "px";
info.textContent = event.pointerType + " at " + Math.round(event.clientX - box.left) + ", " + Math.round(event.clientY - box.top);
});
</script>
Move a pointer over this box
The style touch-action: none tells the browser not to use a finger drag on this element to scroll the page. Without it, a finger starts scrolling, the browser sends pointercancel, and your pointer stops moving.
A drag has three phases: down, move, up. A problem: if the pointer leaves the element while you are dragging, the element stops getting events. element.setPointerCapture(event.pointerId) fixes it: all later events for that pointer go to the element, wherever it moves, until the pointer is released:
<div id="track" style="height:60px; border:1px solid gray; position:relative; touch-action:none;">
<div id="knob" style="width:40px; height:40px; border-radius:6px; background:steelblue; position:absolute; left:0; top:10px; cursor:grab;"></div>
</div>
<p id="out">Drag the square</p>
<script>
const track = document.getElementById("track");
const knob = document.getElementById("knob");
let dragging = false;
knob.addEventListener("pointerdown", function (event) {
dragging = true;
knob.setPointerCapture(event.pointerId);
});
knob.addEventListener("pointermove", function (event) {
if (!dragging) return;
const box = track.getBoundingClientRect();
const x = Math.max(0, Math.min(event.clientX - box.left - 20, box.width - 40));
knob.style.left = x + "px";
document.getElementById("out").textContent = "x = " + Math.round(x);
});
knob.addEventListener("pointerup", function () { dragging = false; });
knob.addEventListener("pointercancel", function () { dragging = false; });
</script>
Drag the square
The line that limits x uses Math.max and Math.min together to clamp a value into a range, a pattern from the Math lesson.
Put it together with a <canvas>, a rectangle you can draw on from JavaScript. While the pointer is down, each move adds a line segment. Because it uses pressure, a pen draws thicker when pressed harder:
<canvas id="pad" width="300" height="140" style="border:1px solid gray; touch-action:none;"></canvas>
<script>
const pad = document.getElementById("pad");
const ctx = pad.getContext("2d");
ctx.lineCap = "round";
let last = null;
pad.addEventListener("pointerdown", function (event) {
pad.setPointerCapture(event.pointerId);
const box = pad.getBoundingClientRect();
last = { x: event.clientX - box.left, y: event.clientY - box.top };
});
pad.addEventListener("pointermove", function (event) {
if (!last) return;
const box = pad.getBoundingClientRect();
const now = { x: event.clientX - box.left, y: event.clientY - box.top };
ctx.lineWidth = 2 + event.pressure * 6;
ctx.beginPath();
ctx.moveTo(last.x, last.y);
ctx.lineTo(now.x, now.y);
ctx.stroke();
last = now;
});
pad.addEventListener("pointerup", function () { last = null; });
pad.addEventListener("pointercancel", function () { last = null; });
</script>
touch-action: none, so a finger drag scrolls the page and the drag ends in pointercancelpointercancel, which leaves a drag "stuck" onmousemove and then wondering why touch screens do nothingclientX as if it were inside the element. It is relative to the viewport, so subtract getBoundingClientRect().leftWhat is the main advantage of pointer events over mouse events?
addEventListenerevent.pointerType tells you which one it was.Why does a drag need setPointerCapture?
What does this log?
const el = document.createElement("div");
el.addEventListener("pointerup", e => console.log(e.pointerType));
el.dispatchEvent(new PointerEvent("pointerup", { pointerType: "pen" }));
pen. The event carries whatever pointerType it was created with.event.pointerTypectx.clearRect(0, 0, 300, 140)touch-action: none and test on a touch screen. What happens?