tap

Tap gesture recognition with multi-tap support. Detects single, double, triple taps and beyond with configurable timing and distance thresholds.

Terminal window
npm install --save @cereb/tap

Basic Usage

import { tap } from "@cereb/tap";
tap(element).on((signal) => {
const { tapCount, x, y } = signal.value;
if (tapCount === 1) {
console.log("Single tap");
} else if (tapCount === 2) {
console.log("Double tap - zoom in!");
}
});

Signature

function tap(target: EventTarget, options?: TapOptions): Stream<TapSignal>

Options

OptionTypeDefaultDescription
movementThresholdnumber10Max movement (px) allowed during tap
durationThresholdnumber500Max duration (ms) for a valid tap
chainMovementThresholdnumbermovementThresholdMax distance between consecutive taps
chainIntervalThresholdnumberdurationThreshold / 2Max interval (ms) between consecutive taps

Multi-Tap Configuration

// Fast double-tap detection
tap(element, {
durationThreshold: 300,
chainIntervalThreshold: 250
})
// Strict tap positioning
tap(element, {
movementThreshold: 5,
chainMovementThreshold: 20
})

Signal Value

The signal.value contains:

PropertyTypeDescription
phase"start" | "end" | "cancel"Current gesture phase
xnumberTap X position (clientX)
ynumberTap Y position (clientY)
pageXnumberTap X position (pageX)
pageYnumberTap Y position (pageY)
tapCountnumberConsecutive tap count (1, 2, 3, …)
durationnumberHow long pointer was pressed (ms)
pointerType"mouse" | "touch" | "pen" | "unknown"Input device type

Phase Lifecycle

pointer down → "start" → pointer up (within thresholds) → "end"
→ moved too far / held too long → "cancel"
  • start: Pointer pressed down
  • end: Valid tap completed (tapCount incremented if chained)
  • cancel: Tap invalidated (moved too far or held too long)

Multi-Tap Chaining

Taps are chained when:

  1. Time between taps is less than chainIntervalThreshold
  2. Distance between tap positions is less than chainMovementThreshold
tap → 200ms → tap → 200ms → tap = tapCount: 3 (triple tap)
tap → 500ms → tap = tapCount: 1 (chain reset)

With Visual Feedback

Use tapRecognizer for full lifecycle handling:

import { singlePointer } from "cereb";
import { tapRecognizer } from "@cereb/tap";
singlePointer(element)
.pipe(tapRecognizer())
.on((signal) => {
const { phase } = signal.value;
if (phase === "start") {
element.classList.add("pressed");
} else {
element.classList.remove("pressed");
}
});

Advanced: tapRecognizer

Use as an operator with custom pointer sources:

import { singlePointer } from "cereb";
import { tapRecognizer } from "@cereb/tap";
singlePointer(element)
.pipe(tapRecognizer({ durationThreshold: 300 }))
.on((signal) => { /* ... */ });

Advanced: tapEndOnly

Only emits successful taps (filters out start/cancel):

import { singlePointer } from "cereb";
import { tapEndOnly } from "@cereb/tap";
singlePointer(element)
.pipe(tapEndOnly())
.on((signal) => {
console.log(`Tap ${signal.value.tapCount}!`);
});

Advanced: createTapRecognizer

Low-level API for imperative usage or custom integrations. The recognizer accepts any signal that satisfies TapSourceSignal interface.

import { createTapRecognizer, type TapSourceSignal } from "@cereb/tap";
const recognizer = createTapRecognizer({ durationThreshold: 300 });
// Works with any source that provides the required properties
function handlePointerEvent(signal: TapSourceSignal) {
const tapEvent = recognizer.process(signal);
if (tapEvent?.value.phase === "end") {
console.log(`Tap ${tapEvent.value.tapCount}!`);
}
}

TapSourceSignal Interface

interface TapSourceSignal {
value: {
phase: "start" | "move" | "end" | "cancel";
x: number;
y: number;
pageX: number;
pageY: number;
pointerType: "touch" | "mouse" | "pen" | "unknown";
};
createdAt: number;
deviceId: string;
}