Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
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.
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.
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 functionconst cleanup = addSwipeListener(element, { threshold: 50 }); // Later: cleanup();/code-block</section> <!-- Swipe Detection --><section id="swipe"> <h2>Swipe Detection</h2> <p>Detects directional swipes by comparing <code>pointerdown</code> and <code>pointerup</code> positions. Dispatches <code>swipe-left</code>, <code>swipe-right</code>, <code>swipe-up</code>, or <code>swipe-down</code> events on the element.</p> <code-block language="js" label="addSwipeListener" data-escape>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);});
| 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.
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.
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});
overflow-y: auto or scrolloverscroll-behavior-y: contain on the container to disable Chrome’s native pull-to-refreshposition: relative on the container for indicator positioningAll listeners use { passive: true } for best scroll performance.
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.
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 — 8mshaptic.confirm(); // confirmation — double pulsehaptic.error(); // validation failure — heavy pulsehaptic.dismiss(); // destructive action — 15ms/code-block <table> <thead> <tr> <th>Method</th> <th>Pattern</th> <th>Use case</th> </tr> </thead> <tbody> <tr> <td><code>haptic.tap()</code></td> <td>8ms</td> <td>Selection, toggle, checkbox</td> </tr> <tr> <td><code>haptic.confirm()</code></td> <td>8-40-8ms</td> <td>Form submit, save, success</td> </tr> <tr> <td><code>haptic.error()</code></td> <td>30-60-30ms</td> <td>Validation failure, error</td> </tr> <tr> <td><code>haptic.dismiss()</code></td> <td>15ms</td> <td>Delete, dismiss, destructive</td> </tr> </tbody> </table></section> <!-- CSS Utilities --><section id="css"> <h2>CSS Utilities</h2> <p>The gesture module includes a companion CSS file that styles gesture states. It’s imported into the <code>utils</code> layer automatically.</p> <code-block language="css" label="Gesture CSS (automatic)" data-escape>/* 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.
View Transition carousels (data-transition) automatically load the gesture module and add swipe navigation. Normal scroll-snap carousels handle touch natively and don’t use gestures.
<!-- VT carousels automatically get swipe navigation --><carousel-wc transition="slide" loop> <article>Slide 1</article> <article>Slide 2</article> <article>Slide 3</article></carousel-wc> <!-- Normal scroll-snap carousels already handle touch natively --><carousel-wc> <article>Slide 1</article> <article>Slide 2</article></carousel-wc>
The gesture module is dynamically imported only when a VT carousel is present, so it adds no overhead to pages without one.
| 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.