The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Start pointer inputsurface.addEventListener('pointerdown', beginGesture)View examples
Check the primary pointerif (!event.isPrimary) returnView examples
Adapt to pointer typeconst kind = event.pointerTypeView examples
Declare handled gesturestouch-action: pan-y pinch-zoom;View examples
Cancel a supported defaultif (event.cancelable) event.preventDefault()View examples
Capture an active pointersurface.setPointerCapture(event.pointerId)View examples
Release pointer capturesurface.releasePointerCapture(pointerId)View examples
Handle lost capturesurface.addEventListener('lostpointercapture', cancelGesture)View examples
Track active contactsactivePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })View examples
Read normalized pressureconst pressure = event.pressureView examples
Read coalesced samplesconst samples = event.getCoalescedEvents?.() ?? [event]View examples
Handle raw updates conditionallysurface.addEventListener('pointerrawupdate', drawSamples)View examples
Handle gesture cancellationsurface.addEventListener('pointercancel', cancelGesture)View examples
Remove gesture listenerscontroller.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

01

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.

Track one drag by pointer identifier
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);
});
Back to quick reference ↑
02

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.

Reserve horizontal dragging without blocking page zoom
.timeline {
  touch-action: pan-y pinch-zoom;
  user-select: none;
}
.timeline button {
  touch-action: manipulation;
}
Back to quick reference ↑
03

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.

Capture a slider drag
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'));
Back to quick reference ↑
04

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.

Maintain a bounded active-pointer map
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);
Back to quick reference ↑
05

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.

Batch samples into one animation frame
let pending = [];
let frame = 0;
function collect(event) {
  pending.push(...(event.getCoalescedEvents?.() ?? [event]));
  frame ||= requestAnimationFrame(() => {
    drawSamples(pending);
    pending = [];
    frame = 0;
  });
}
surface.addEventListener('pointermove', collect);
Back to quick reference ↑
06

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.

Install removable listeners with cancellation
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();
}
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. World Wide Web ConsortiumPointer Events Level 3w3.org
  2. Pointer Events Working GroupPointer Events Level 4: Editors' Draftw3c.github.io
  3. World Wide Web ConsortiumTouch Eventsw3.org
  4. World Wide Web ConsortiumCSS Basic User Interface Module Level 4w3.org
  5. 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.

Share feedback