tap

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

Basic Usage

import { tap } from "cereb";
tap(element).on((signal) => {
const { tapCount, cursor } = signal.value;
const [x, y] = cursor;
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
cursor[number, number]Tap position (client coordinates)
pageCursor[number, number]Tap position (page coordinates)
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 operator for full lifecycle handling:

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

Filter to End Phase Only

Use tapEndOnly to only receive successful taps:

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