A lightweight pointer event library for handling mouse, touch, and pen interactions in browsers. Provides unified APIs for down, move, click, long, hover, drag, resize and more.
- 🖱️ Unified pointer event handling (mouse, touch, pen)
- 📱 Mobile-friendly with touch support
- 🎯 Click, double-click, and long-press detection
- 👆 Hover detection with enter/move/leave callbacks
- 🔄 Drag and drop with customizable constraints
- 📐 Resize functionality with border detection
- 🎨 Global cursor management
- 🪝 Global hooks for move events
- 📦 Zero dependencies
- 🔷 Full TypeScript support
- 🌐 ESM and UMD bundle support
npm install @litert/pointer<!-- UMD (Development) -->
<script src="https://unpkg.com/@litert/pointer/dist/index.umd.js"></script>
<!-- UMD (Production, minified) -->
<script src="https://unpkg.com/@litert/pointer/dist/index.umd.min.js"></script>import * as pointer from '@litert/pointer';
element.addEventListener('pointerdown', (e) => {
pointer.move(e, {
move: (e, detail) => {
console.log('Moving:', detail.ox, detail.oy);
}
});
});<script src="https://unpkg.com/@litert/pointer/dist/index.umd.min.js"></script>
<script>
element.addEventListener('pointerdown', function(e) {
pointer.move(e, {
move: function(e, detail) {
console.log('Moving:', detail.ox, detail.oy);
}
});
});
</script>Down and up events, bind to pointer events.
pointer.down(e, {
down: (e) => { /* Triggered when pressed */ },
start: (e) => { /* Triggered when start moving, return false to cancel subsequent events */ },
move: (e, dir) => { /* Triggered when moving, dir is the direction of movement */ },
up: (e) => { /* Triggered when released */ },
end: (e) => { /* Triggered when moving ends */ }
});Click takes effect only when the pointer does not move and the time is less than 250ms.
element.addEventListener('pointerdown', (e) => {
pointer.click(e, (e, x, y) => {
console.log('Clicked at:', x, y);
});
});Double-click event, the interval between two clicks is less than 300ms and the position difference is less than 10px.
element.addEventListener('pointerdown', (e) => {
pointer.dblClick(e, (e, x, y) => {
console.log('Double clicked at:', x, y);
});
});Long press event, default 300ms.
element.addEventListener('pointerdown', (e) => {
pointer.long(e, (e) => {
console.log('Long press detected!');
}, { 'time': 500 });
});Hover event, handles enter, move, and leave for mouse and touch.
const cb = (e) => {
pointer.hover(e, {
enter: (e) => {
console.log('Pointer entered');
},
move: (e) => {
console.log('Pointer moving at:', e.clientX, e.clientY);
},
leave: (e) => {
console.log('Pointer left');
}
});
};
element.addEventListener('pointerdown', cb);
element.addEventListener('pointerenter', cb);Drag event, supports boundary detection and constraints.
element.addEventListener('pointerdown', (e) => {
pointer.move(e, {
// --- Constraint area ---
left: 0,
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
// --- Or use an element as a constraint area ---
// areaObject: document.getElementById('container'),
// --- Drag object (used to calculate boundary offset) ---
object: element,
// --- Callbacks ---
start: (x, y) => {
console.log('Start at:', x, y);
},
move: (e, detail) => {
console.log('Move:', detail.ox, detail.oy);
console.log('Position:', detail.x, detail.y);
console.log('Border:', detail.border);
console.log('Direction:', detail.dir);
},
up: (moveTimes, e) => {
console.log('Up');
},
end: (moveTimes, e) => {
console.log('End');
},
borderIn: (x, y, border, e) => {
console.log('Border in:', border);
},
borderOut: () => {
console.log('Border out');
}
});
});Global hooks for move events. These hooks are called for all move operations.
// --- Register global move down hook ---
pointer.addMoveHook('down', (e, opt) => {
console.log('Global move down hook:', e, opt);
});
// --- Register global move up hook ---
pointer.addMoveHook('up', (e, opt) => {
console.log('Global move up hook:', moveTimes, e, opt);
});
// --- Remove a hook ---
pointer.removeMoveHook('down', hookFunction);Resize event.
element.addEventListener('pointerdown', (e) => {
pointer.resize(e, {
object: element,
border: 'rb', // lt, t, tr, r, rb, b, bl, l
minWidth: 100,
minHeight: 100,
maxWidth: 500,
maxHeight: 500,
start: (x, y) => {
console.log('Resize start');
},
move: (left, top, width, height, x, y, border) => {
element.style.left = left + 'px';
element.style.top = top + 'px';
element.style.width = width + 'px';
element.style.height = height + 'px';
},
end: () => {
console.log('Resize end');
}
});
});Drag event, supports drag and drop to target elements. Returns an idempotent cancel
function; call it when the source component unmounts. signal accepts an
AbortSignal, and threshold defaults to 5 CSS pixels. A single ghost follows the
pointer while the source stays in place.
start(x, y), move(event, detail) and end(moveTimes, event) retain their existing
arguments. Returning false from start rejects the drag. move also runs over
receivers; end runs after an actual drag ends with a normal pointer release.
finish(event, detail) runs once after cleanup, including clicks, rejected starts
and cancellations. detail contains started, cancelled, reason and target.
Reasons are up, cancel, blur, abort, dispose, escape and reject.
The event can be absent for programmatic cancellation or focus loss.
Only the original pointer moves or finishes a drag. A second pointer, Escape,
window blur, pointer cancellation or explicit cancellation stops it without a
drop. Drop targets use data-drop; hit testing checks the topmost element and its
ancestors, so a covered target cannot receive a drop. The target is checked again
at release. Events expose the payload in detail.value and the originating
pointer event in detail.event.
// --- Set drag source ---
dragSource.addEventListener('pointerdown', (e) => {
pointer.drag(e, dragSource, {
data: { id: 1, name: 'item' },
start: (x, y) => {
console.log('Drag start');
},
move: (e, detail) => {
console.log('Dragging');
},
end: (moveTimes, e) => {
console.log('Drag end');
},
finish: (e, detail) => {
console.log(detail.cancelled ? 'Cancelled' : 'Finished', detail.target);
}
});
});
// --- Set drop target ---
dropTarget.dataset.drop = '';
dropTarget.addEventListener('dragenter', (e) => {
console.log('Drag enter');
});
dropTarget.addEventListener('dragleave', (e) => {
console.log('Drag leave');
});
dropTarget.addEventListener('drop', (e) => {
const data = pointer.getDragData();
console.log('Dropped:', data);
});down, long and menu also return cancel functions. move retains its boundary
result and adds result.cancel(). down and move support signal, threshold
(default 0), single (cancel when another pointer goes down), escape and
finish(event, detail); move.finish additionally receives the movement history as
its third argument. Cleanup runs on blur as well as pointer release. The legacy
up/end callbacks still receive pointer events; use finish for all exit paths.
Tracks single-pointer panning, multi-touch pinch zoom, and mouse-wheel zoom. Existing
scale(e, (e, scale, cpos) => ...) calls continue to work. The return value is an
idempotent function that ends the active pointer gesture and removes its listeners.
The handler receives (event, scale, cpos, detail):
scale: positive incremental multiplier,1for panning.cpos: incremental movement of the center, not its absolute position.detail.mode:'pan','pinch', or'wheel'.detail.center/detail.previousCenter: current and previous center in viewport CSS pixels, in the same coordinate system asclientX/clientY. Wheel events use the mouse position for both centers.detail.pointers: number of pressed pointers in this gesture;0for wheel events.
Options:
target: gesture region. Defaults to the event'scurrentTargetelement, then itstargetelement. Supply it explicitly when forwarding events from a document listener or invokingscaleafter event dispatch.signal: anAbortSignalfor teardown, for example when a component unmounts.onPointers(event, count): called immediately when a pointer joins or is released, including the initial count of1and normal completion at0. An editor can use the transition to2to cancel its pending single-pointer edit.onEnd(event, reason): called once after pointer-session cleanup. Reasons are'up','cancel','blur','abort', and'dispose'. The event is absent for abort/dispose. Use this callback to reset interaction state on every exit path. Wheel events are synchronous and do not start a session or call these callbacks.
Each region has one active pointer session, using its first call's handler and
options. Subsequent pointer downs join that session; they do not add duplicate
listeners. A pointer must begin inside the region, and cannot belong to two
sessions. The first two active pointers drive the pinch; extra pointers can take
over when one is lifted, without a positional jump. Any participating pointer's
pointercancel, window blur, abort, or disposal ends the entire session.
Set touch-action: none on the gesture region before touch starts, so the
browser does not take over with native scrolling. Register the wheel listener as
non-passive. The library does not change element styles or apply transforms.
// CSS: .viewport { touch-action: none; overflow: hidden; }
// CSS: .content { transform-origin: 0 0; }
// The viewport is untransformed and the content starts at its inner top-left.
let zoom = 1;
let offset = { x: 0, y: 0 };
const lifetime = new AbortController();
const handler: pointer.TScaleHandler = (event, factor, cpos, detail) => {
const rect = element.getBoundingClientRect();
const origin = { x: rect.left + element.clientLeft, y: rect.top + element.clientTop };
const nextZoom = Math.max(0.05, Math.min(20, zoom * factor));
const applied = nextZoom / zoom;
// Keep the content point under the gesture center fixed while zooming.
offset = {
x: detail.center.x - origin.x - (detail.previousCenter.x - origin.x - offset.x) * applied,
y: detail.center.y - origin.y - (detail.previousCenter.y - origin.y - offset.y) * applied
};
zoom = nextZoom;
content.style.transform = `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`;
};
element.addEventListener('pointerdown', (event) => {
pointer.scale(event, handler, { signal: lifetime.signal });
}, { signal: lifetime.signal });
element.addEventListener('wheel', (event) => {
pointer.scale(event, handler, { signal: lifetime.signal });
}, { passive: false, signal: lifetime.signal });
// On unmount: removes the application's listeners and cancels any active gesture.
// lifetime.abort();Wheel pixel, line, and page deltas are normalized (one line = 16 CSS pixels; one
page = the region height). Exponential scaling keeps large deltas positive and
bounds a single event to a multiplier between 1/e and e. Equal opposite deltas
cancel each other. Absolute zoom limits, fit-to-view, and UI state synchronization
belong to the consuming control.
Gesture event for swipe actions (up, down, left, right).
element.addEventListener('pointerdown', (e) => {
pointer.gesture(e, (e, dir) => {
// Return 1 to show gesture indicator, 0 to ignore, -1 to stop propagation
return 1;
}, (dir) => {
console.log('Gesture completed:', dir);
});
});
element.addEventListener('wheel', (e) => {
pointer.gesture(e, (e, dir) => {
return 1;
}, (dir) => {
console.log('Wheel gesture:', dir);
});
});Set/cancel global mouse style.
pointer.setCursor('move');
// ...
pointer.setCursor(); // CancelCheck if the pointer event is from a touch device.
const isTouch = pointer.isTouch(e);
console.log('Is touch device:', isTouch);Direction type.
type TDirection = 'top' | 'right' | 'bottom' | 'left';Border direction type.
type TBorder = 'lt' | 't' | 'tr' | 'r' | 'rb' | 'b' | 'bl' | 'l' | '';Options for the down function.
interface IDownOptions {
down?: (e: PointerEvent) => void;
start?: (e: PointerEvent) => any;
move?: (e: PointerEvent, dir: TDirection) => any;
up?: (e: PointerEvent) => void | Promise<void>;
end?: (e: PointerEvent) => void | Promise<void>;
}Options for the hover function.
interface IHoverOptions {
enter?: (e: PointerEvent) => void;
move?: (e: PointerEvent) => void;
leave?: (e: PointerEvent) => void;
}Options for the long function.
interface ILongOptions {
time?: number; // Long press time, default 300 ms
down?: (e: PointerEvent) => void | Promise<void>;
up?: (e: PointerEvent) => void | Promise<void>;
}Detailed information in the move callback.
interface IMoveDetail {
ox: number; // x-axis offset
oy: number; // y-axis offset
x: number; // Current x coordinate
y: number; // Current y coordinate
border: TBorder; // Border type
inBorder: { // Whether at the border
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
};
dir: TDirection; // Direction of movement
}Movement time record.
interface IMoveTime {
time: number;
ox: number;
oy: number;
}Options for the move function.
interface IMoveOptions {
areaObject?: HTMLElement;
left?: number;
top?: number;
right?: number;
bottom?: number;
offsetLeft?: number;
offsetTop?: number;
offsetRight?: number;
offsetBottom?: number;
objectLeft?: number;
objectTop?: number;
objectWidth?: number;
objectHeight?: number;
object?: HTMLElement;
cursor?: string;
start?: (x: number, y: number) => any;
move?: (e: PointerEvent, detail: IMoveDetail) => void;
borderIn?: (x: number, y: number, border: TBorder, e: PointerEvent) => void;
borderOut?: () => void;
up?: (moveTimes: IMoveTime[], e: PointerEvent) => void;
end?: (moveTimes: IMoveTime[], e: PointerEvent) => void;
}Result returned by the move function.
interface IMoveResult {
left: number;
top: number;
right: number;
bottom: number;
}Options for the resize function.
interface IResizeOptions {
border: TBorder;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
object?: HTMLElement;
objectLeft?: number;
objectTop?: number;
objectWidth?: number;
objectHeight?: number;
start?: (x: number, y: number) => any;
move?: (left: number, top: number, width: number, height: number, x: number, y: number, border: TBorder) => void;
end?: (moveTimes: IMoveTime[], e: PointerEvent) => void;
}Options for the drag function.
interface IDragOptions {
data?: any;
start?: (x: number, y: number) => any;
move?: (e: PointerEvent, detail: IMoveDetail) => void;
end?: (moveTimes: IMoveTime[], e: PointerEvent) => void;
}Handler function type for scale events.
type TScaleHandler = (e: PointerEvent | WheelEvent, scale: number, cpos: { x: number; y: number; }) => void | Promise<void>;Before handler function type for gesture events. Return 1 to show gesture, 0 to ignore, -1 to stop propagation.
type TGestureBeforeHandler = (e: PointerEvent | WheelEvent, dir: TDirection) => number;Handler function type for gesture events.
type TGestureHandler = (dir: TDirection) => void | Promise<void>;Handler function type for menu events.
type TMenuHandler = (e: PointerEvent | MouseEvent) => void | Promise<void>;Global hook function type for move down events.
type TMoveDownHook = (e: PointerEvent, opt: IMoveOptions) => void | Promise<void>;Global hook function type for move up events.
type TMoveUpHook = (e: PointerEvent, opt: IMoveOptions) => void | Promise<void>;npm install
npm run check
npm test
npm run build
npm pack --dry-runnpm run check runs tsc --noEmit. The default TypeScript configuration also sets
noEmit: true, so running tsc directly only checks types and writes no files.
npm run build uses tsconfig.build.json to generate the library's declaration
files under dist/src/, then Rollup creates the ESM, UMD, and minified UMD bundles.
The per-module .d.ts files are required by the package's type entry; per-module
.js files and compiled build scripts are not distribution artifacts. Node.js
types are required by the build script; the library itself runs in the browser.
The npm package includes only these bundles and dist/src/**/*.d.ts, plus the
standard package metadata, README, and license. Source files, build scripts,
tests, and demos are excluded from the publish allowlist.
Clone the repository and open dist/test/index.html in your browser.
git clone https://github.com/litert/pointer.js.git
cd pointer.js
npm install
npm run buildThen open dist/test/index.html in your browser.
- Chrome 55+
- Firefox 59+
- Safari 13+
- Edge 79+
- Mobile browsers (iOS Safari 13+, Chrome for Android)
Requires Pointer Events API support. For older browsers, consider using a polyfill.
This library is published under Apache-2.0 license.