The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Start pointer input | surface.addEventListener('pointerdown', beginGesture) | View examples |
| Check the primary pointer | if (!event.isPrimary) return | View examples |
| Adapt to pointer type | const kind = event.pointerType | View examples |
| Declare handled gestures | touch-action: pan-y pinch-zoom; | View examples |
| Cancel a supported default | if (event.cancelable) event.preventDefault() | View examples |
| Capture an active pointer | surface.setPointerCapture(event.pointerId) | View examples |
| Release pointer capture | surface.releasePointerCapture(pointerId) | View examples |
| Handle lost capture | surface.addEventListener('lostpointercapture', cancelGesture) | View examples |
| Track active contacts | activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) | View examples |
| Read normalized pressure | const pressure = event.pressure | View examples |
| Read coalesced samples | const samples = event.getCoalescedEvents?.() ?? [event] | View examples |
| Handle raw updates conditionally | surface.addEventListener('pointerrawupdate', drawSamples) | View examples |
| Handle gesture cancellation | surface.addEventListener('pointercancel', cancelGesture) | View examples |
| Remove gesture listeners | controller.abort() | View examples |
Pointer Events unify mouse, touch, pen, and other pointing devices while preserving device-specific details. Correct gesture handling combines event state with CSS touch-action, pointer capture, cancellation, and semantic keyboard-accessible controls. Track pointerId values only for an active gesture, do not infer identity from device fields, and always recover from pointercancel or lost capture.
Step by step
Detailed examples
Model a gesture as a complete pointer event sequence
pointerdown begins active buttons or contact, pointermove updates it, and pointerup normally ends it. pointerover/out bubble while pointerenter/leave do not. pointerId is stable only for the pointer's active lifetime and can be reused; persistentDeviceId has privacy considerations and limited availability. isPrimary means primary within a pointer type, not the only active input. For mouse buttons inspect buttons during movement rather than relying only on button.
let drag = null;
surface.addEventListener('pointerdown', event => {
if (event.button !== 0 || drag) return;
drag = { id: event.pointerId, x: event.clientX, y: event.clientY };
});
surface.addEventListener('pointermove', event => {
if (event.pointerId === drag?.id) updateDrag(event.clientX, event.clientY);
}); Declare browser gesture ownership with touch-action
The browser determines permitted touch behavior by intersecting touch-action values from the hit-tested element through the relevant scrolling ancestor when a gesture begins. Changing it mid-gesture does not change that decision. Use auto when native behavior should win, manipulation for standard pan and pinch without nonstandard gestures, and precise pan directions when the page owns the remainder. Avoid touch-action none on broad regions because disabling pinch zoom harms accessibility.
.timeline {
touch-action: pan-y pinch-zoom;
user-select: none;
}
.timeline button {
touch-action: manipulation;
} Capture after pointerdown and clean up when capture is lost
setPointerCapture() keeps later pointer events targeted at an element even when the pointer leaves its bounds, which stabilizes sliders and drags. Call it only for an active pointer; otherwise NotFoundError can occur. gotpointercapture and lostpointercapture expose transitions. Capture changes event targeting, not coordinates or hit testing, and it does not grant exclusive access to device input. Remove or reset state on pointerup, pointercancel, and lostpointercapture.
slider.addEventListener('pointerdown', event => {
slider.setPointerCapture(event.pointerId);
updateSlider(event.clientX);
});
slider.addEventListener('pointermove', event => {
if (slider.hasPointerCapture(event.pointerId)) updateSlider(event.clientX);
});
slider.addEventListener('lostpointercapture', () => slider.classList.remove('dragging')); Calculate multi-pointer gestures from active state
Maintain a Map keyed by pointerId and update it for each down, move, up, cancel, and lost-capture event. Start a two-pointer transform from a snapshot of initial distance, angle, and midpoint; do not compound each noisy delta onto the last rendered transform. Additional contacts, palm rejection, and device changes need explicit policy. Width, height, pressure, tilt, twist, and tangentialPressure are capabilities, not reliable biometric or authentication signals.
const active = new Map();
function remember(event) { active.set(event.pointerId, { x: event.clientX, y: event.clientY }); }
function forget(event) { active.delete(event.pointerId); renderGesture(active); }
surface.addEventListener('pointerdown', remember);
surface.addEventListener('pointermove', event => { if (active.has(event.pointerId)) remember(event); });
surface.addEventListener('pointerup', forget);
surface.addEventListener('pointercancel', forget); Process high-frequency input without overwhelming rendering
User agents may coalesce multiple hardware samples into one pointermove; getCoalescedEvents() exposes them where supported. pointerrawupdate can arrive with lower latency and at high frequency but remains less widely available. Buffer samples and render at most once per requestAnimationFrame, reuse arrays where practical, and keep handlers short. Coordinates can be fractional. predicted events are speculative and must never be committed as final ink or authoritative input.
let pending = [];
let frame = 0;
function collect(event) {
pending.push(...(event.getCoalescedEvents?.() ?? [event]));
frame ||= requestAnimationFrame(() => {
drawSamples(pending);
pending = [];
frame = 0;
});
}
surface.addEventListener('pointermove', collect); Preserve semantic input and cancel every exit path
A canvas gesture needs an accessible alternative: semantic buttons, range inputs, keyboard operations, visible focus, and non-pointer instructions. Do not replace click for ordinary activation; click preserves keyboard and assistive-technology behavior. pointercancel can occur for scrolling, orientation, palm rejection, hardware changes, or too many contacts. Use AbortSignal-backed listeners, release app state on cancellation or lost capture, cancel pending animation frames, and avoid recording precise pen or touch telemetry without need and consent.
const controller = new AbortController();
const options = { signal: controller.signal };
surface.addEventListener('pointerup', finishGesture, options);
surface.addEventListener('pointercancel', cancelGesture, options);
surface.addEventListener('lostpointercapture', cancelGesture, options);
function dispose() {
controller.abort();
cancelAnimationFrame(frame);
active.clear();
} Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- World Wide Web ConsortiumPointer Events Level 3w3.org
- Pointer Events Working GroupPointer Events Level 4: Editors' Draftw3c.github.io
- World Wide Web ConsortiumTouch Eventsw3.org
- World Wide Web ConsortiumCSS Basic User Interface Module Level 4w3.org
- WHATWGHTML Standard: Activation Behaviorhtml.spec.whatwg.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



