scurry-n-slide
v1.0.1
Published
Pointer capture; choose your own abstraction.
Downloads
458
Maintainers
Readme
Scurry’n’slide — pointer capture; choose your own abstraction
A pointer tracker for mouse, pen, and touch. It handles pointer capture and
concurrent pointers, each with its own state. The callbacks receive the
PointerEvent as-is, along with the state you assigned to that pointer. The
rest is up to your application — no naming conventions, patterns, or
execution style — nothing is forced on you.
Since the interaction definition lives in your code, removing this library at any point in the future means replacing the pointer bookkeeping, not rewriting the interaction. The callback shape is quite common, it won’t stay in the way.
Check the demos on the project page: https://myshkin.eu/scurry-n-slide
Usage
Install the package or copy the distributable file into a project:
npm install scurry-n-slideThe package is a native ECMAScript module:
import trackPointers from "scurry-n-slide";
const ball = document.querySelector(".ball");
const cleanup = trackPointers(ball, {
start(event) {
if (event.button !== 0) return false;
return {
pointerX: event.clientX,
pointerY: event.clientY,
x: Number(ball.dataset.x || 0),
y: Number(ball.dataset.y || 0),
};
},
move(event, state) {
const x = state.x + event.clientX - state.pointerX;
const y = state.y + event.clientY - state.pointerY;
ball.dataset.x = x;
ball.dataset.y = y;
ball.style.transform = `translate(${x}px, ${y}px)`;
},
end(event) {
if (event?.type === "pointerup") {
console.log("completed");
} else {
console.log("cancelled");
}
},
});
// The cleanup function could be passed to the owning component’s
// destructor. Calling it during an interaction ends every active
// pointer with an undefined event.
function destroy() {
cleanup();
}The cleanup function is the return value, so it drops straight into a host that already expects a teardown function:
useEffect(() => trackPointers(ref.current, { start, move, end }), []);Configure the area that starts the interaction in CSS, before it begins:
.ball {
touch-action: none;
user-select: none;
}Choose the touch-action value to match the effect you want.
API
trackPointers(element, { start, move, end }) -> cleanupstart(event)
Runs for each pointerdown that reaches element.
- Return
falseto reject the pointer. The tracker will not request capture or deliver further callbacks. Browsers may still apply native implicit capture to direct inputs such as touch. - Any other return value is accepted as that pointer’s state and is passed
unchanged to
moveandend. - The callback is synchronous. A returned promise is treated as state and is not awaited.
move(event, state)
Runs for each captured pointermove. The original PointerEvent is passed
as-is.
Simultaneous pointers have independent state values. Interaction between
pointers (multi-pointer gestures) could be handled in an outer scope.
end(event, state)
Runs exactly once for every accepted pointer.
- A
pointerupevent is a regular completion. pointercancel,lostpointercapture, and browser recovery events are cancellations. The actual DOM event is passed through.- When no per-pointer causal DOM event exists, such as programmatic cleanup or
document adoption,
endreceivesundefined.
cleanup()
Stops future interactions, releases active captures, and calls end once for
each active pointer with an undefined event. Repeated calls do nothing.
TypeScript
Types ship with the package as a *.d.ts file. State is inferred from the
return value of start.
trackPointers(ball, {
start: (event) => ({ from: event.clientX }),
move: (event, state) => state.from, // state is { from: number }
});What it leaves to you
Scurry’n’slide does not call preventDefault, stop event propagation, change
styles, filter input, or suppress clicks. In particular:
- The tracker never sets
touch-action. Where it belongs depends on your layout and stacking, which the tracker cannot see. - Pointer capture retargets pointer events to the tracked element, so
event.targetis that element and not whatever sits under the pointer.
Errors
trackPointers throws only where you call it. A TypeError means the element
does not support pointer capture or a handler is not a function, and installing
the pointerdown listener may propagate a native error from the supplied
target. All of it happens before the call returns, where the calling code can
fix or handle it.
After registration succeeds, the tracker does not throw:
- Pointer-capture, document, iframe, and listener failures cancel affected
interactions through
end. cleanup()remains non-throwing and attempts to end every active pointer.- Exceptions from consumer
start,move, andendcallbacks are reported unchanged through the owner window’s standardreportError()mechanism. This keeps them visible to developer tools and global error monitoring without turning them into library control flow.
A throwing start does not accept its pointer. A throwing move leaves its
pointer active. A pointer stops being tracked before its end runs, so a
throwing end cannot leave it stuck.
Engine support
The target is current evergreen browsers with Pointer Events and pointer capture. The project doesn’t provide a mouse/touch fallback for legacy browsers.
Importing the module does not access browser globals. Calling trackPointers
requires a browser element that supports pointer capture.
Vendoring
The whole project fits into dist/scurry-n-slide.js — a self-contained file
generated from this documentation, LICENSE, and index.js. Copy
dist/scurry-n-slide.d.ts alongside it to keep the types.
Do not edit the generated files directly. Run:
npm run buildStory
The project grew out of a gist written in August 2022 and used in various projects as a base for drag interactions. The shape settled early. This repo fixes a few inherent shortcomings and packages the result.
License
MIT
