Gestures

Touch gesture library for swipe detection, swipe-to-dismiss, pull-to-refresh, long-press, and haptic feedback. Pointer-event based, works on touch, mouse, and pen.

Overview

The gesture module (src/lib/vb-gestures.js) provides five named exports:

Export Purpose
addSwipeListener Detect swipe direction on an element
makeSwipeable Drag-to-dismiss with visual feedback
addPullToRefresh Pull-down spinner with async callback
addLongPress Timer-based long-press detection
haptic Vibration patterns for feedback (Android)

Every function returns a cleanup function for teardown. All use PointerEvent (not TouchEvent) and check e.isPrimary to ignore multi-touch.

Installation

The module lazy-loads automatically when any data-gesture attribute exists on the page. Just add the attribute and gestures work:

<!-- Auto-loads when data-gesture attributes are present --> <article data-gesture="swipe">Swipe me</article> <article data-gesture="dismiss">Dismiss me</article> <figure data-gesture="long-press">Hold me</figure>

For programmatic use (like pull-to-refresh which requires a callback), import the module directly:

import { addSwipeListener, makeSwipeable, addPullToRefresh, addLongPress, haptic } from '/src/lib/vb-gestures.js'; // Each returns a cleanup function const cleanup = addSwipeListener(element, { threshold: 50 }); // Later: cleanup();

Swipe Detection

Detects directional swipes by comparing pointerdown and pointerup positions. Dispatches swipe-left, swipe-right, swipe-up, or swipe-down events on the element.

const cleanup = addSwipeListener(element, { threshold: 50, // min distance (px) restraint: 100, // max perpendicular distance (px) timeout: 300 // max duration (ms) }); element.addEventListener('swipe-left', (e) => { console.log('Swiped left!', e.detail.distance, e.detail.duration); }); element.addEventListener('swipe-right', (e) => { console.log('Swiped right!', e.detail); });

Options

Option Default Description
threshold 50px Minimum distance to qualify as a swipe
restraint 100px Maximum perpendicular distance
timeout 300ms Maximum time between down and up

The 50px default threshold avoids conflict with iOS Safari’s edge-swipe back gesture, which activates from the left ~20px.

Swipe-to-Dismiss

Full drag tracking with pointer capture. The element translates and fades as you drag, then either snaps back or slides off screen.

const cleanup = makeSwipeable(card, { threshold: 100, // distance to trigger dismiss direction: 'horizontal', // 'horizontal' or 'vertical' removeOnDismiss: true // remove element from DOM }); card.addEventListener('swipe-dismiss', (e) => { console.log('Dismissed:', e.detail.direction); });

State is managed via data attributes: data-swiping during drag, data-dismissed after dismiss. The snap-back uses a spring easing for a natural feel.

Pull-to-Refresh

Creates a spinner indicator inside a scroll container. The spinner appears when pulled past threshold; your async callback runs while the spinner shows.

const cleanup = addPullToRefresh(scrollContainer, async () => { const data = await fetch('/api/items'); const items = await data.json(); renderItems(items); }, { threshold: 70, // pull distance to trigger maxPull: 120 // maximum pull distance });

Requirements

  • Container must have overflow-y: auto or scroll
  • Add overscroll-behavior-y: contain on the container to disable Chrome’s native pull-to-refresh
  • Add position: relative on the container for indicator positioning

All listeners use { passive: true } for best scroll performance.

Long Press

Timer-based detection: starts a setTimeout on pointerdown, cancels on pointermove, pointerup, or pointercancel.

const cleanup = addLongPress(element, (e) => { // Enter select mode, show context menu, etc. element.toggleAttribute('data-selected'); }, { duration: 500, // hold time in ms hapticFeedback: true, // vibrate on trigger (Android) blockContextMenu: true // prevent native context menu });

Use data-gesture="long-press" for declarative usage — the element dispatches a long-press custom event that you can listen for.

Haptic Feedback

Four vibration patterns for tactile feedback. Uses the Vibration API, which is Android-only — calls no-op silently on iOS and desktop.

import { haptic } from '/src/lib/vb-gestures.js'; haptic.tap(); // selection, toggle — 8ms haptic.confirm(); // confirmation — double pulse haptic.error(); // validation failure — heavy pulse haptic.dismiss(); // destructive action — 15ms
Method Pattern Use case
haptic.tap() 8ms Selection, toggle, checkbox
haptic.confirm() 8-40-8ms Form submit, save, success
haptic.error() 30-60-30ms Validation failure, error
haptic.dismiss() 15ms Delete, dismiss, destructive

CSS Utilities

The gesture module includes a companion CSS file that styles gesture states. It’s imported into the utils layer automatically.

/* Applied automatically by the gesture module */ [data-swiping] { user-select: none; will-change: transform, opacity; cursor: grabbing; } [data-dismissed] { pointer-events: none; } /* Touch-action hints (add to your elements) */ [data-gesture="swipe"], [data-gesture="dismiss"] { touch-action: pan-y; } /* Pull-to-refresh container */ [data-gesture="pull-refresh"] { touch-action: pan-x; overscroll-behavior-y: contain; position: relative; }

Reduced-motion users see no spinner animation, and will-change is removed to avoid unnecessary compositing.

Browser Support

Feature Chrome Firefox Safari Edge
PointerEvent 55+ 59+ 13+ 12+
setPointerCapture 55+ 59+ 13+ 12+
touch-action 36+ 52+ 10+ 12+
Vibration API 32+ 16+ No 79+

All gesture features work in all modern browsers. Haptic feedback is Android-only; the haptic object no-ops silently on platforms without Vibration API support.