# How to Implement Velocity-Based Swipe Dismissal in Web UI

> Implement velocity-based swipe dismissal in web UI. Learn how to combine distance and velocity thresholds for intuitive drag and flick gestures to dismiss elements.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-07-31

---

**Velocity-based swipe dismissal combines distance and velocity thresholds to determine when a user intends to dismiss a draggable element, enabling both slow drags past a threshold and quick flicks to close components.**

The `emilkowalski/skills` repository defines this interaction pattern in its **Emil Design Engineering** guidelines, providing a robust alternative to distance-only swipe detection. By calculating the ratio of displacement to time, you can capture rapid gestures that travel only a short distance but clearly indicate user intent to dismiss.

## Core Concepts of Velocity-Based Dismissal

Traditional swipe implementations rely solely on a static distance threshold, forcing users to drag elements significant distances even when they intend a quick "flick" action. The velocity-based approach adds a temporal dimension to gesture recognition.

### Calculating Swipe Velocity

According to the implementation details in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md), velocity is computed as a simple ratio of distance over time. When a drag gesture begins, record the start timestamp (`dragStartTime`). Upon release, calculate the elapsed time (`timeTaken`) and divide the absolute swipe displacement by this duration:

```javascript
const velocity = Math.abs(swipeAmount) / timeTaken; // px per millisecond

```

This yields a velocity measured in pixels per millisecond, providing a quantitative measure of gesture speed regardless of total distance traveled.

### The Dual-Threshold Logic

The dismissal decision uses an OR condition that checks both spatial and temporal criteria. As specified in the source guidelines, the element should dismiss when either:

- The absolute distance exceeds `SWIPE_THRESHOLD` (typically **30 pixels**), **or**
- The calculated velocity exceeds **0.11 px/ms**

```javascript
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
  dismiss();
}

```

This dual-threshold approach ensures that slow, deliberate drags still work while enabling rapid flicks to dismiss drawers, toasts, or cards without requiring full travel distance.

## Implementation Patterns

### Plain JavaScript Implementation

For vanilla JavaScript implementations using the Pointer Events API, track the drag state across `pointerdown`, `pointermove`, and `pointerup` events:

```javascript
let dragStartTime = null;
let swipeAmount = 0;
const SWIPE_THRESHOLD = 30; // px – adjust to your UI

function onPointerDown(e) {
  dragStartTime = Date.now();
  swipeAmount = 0;
  element.setPointerCapture(e.pointerId);
}

function onPointerMove(e) {
  if (dragStartTime === null) return;
  swipeAmount = e.clientX - e.target.offsetLeft;
  // Apply visual feedback using transform for GPU acceleration
  element.style.transform = `translateX(${swipeAmount}px)`;
}

function onPointerUp(e) {
  if (dragStartTime === null) return;
  const timeTaken = Date.now() - dragStartTime;
  const velocity = Math.abs(swipeAmount) / timeTaken;

  if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
    dismiss(); // Your dismissal logic here
  } else {
    // Snap back to original position
    element.style.transition = 'transform 200ms ease-out';
    element.style.transform = 'translateX(0)';
  }

  dragStartTime = null;
}

```

### React Hook Implementation

For React applications, encapsulate the logic in a custom hook using `useRef` to persist values across renders without triggering re-renders:

```jsx
import { useRef } from 'react';

function useSwipeDismiss({ onDismiss, threshold = 30 }) {
  const startTime = useRef(null);
  const swipeAmt = useRef(0);
  const ref = useRef(null);

  const onPointerDown = (e) => {
    startTime.current = Date.now();
    swipeAmt.current = 0;
    ref.current.setPointerCapture(e.pointerId);
  };

  const onPointerMove = (e) => {
    if (startTime.current === null) return;
    swipeAmt.current = e.clientX - ref.current.offsetLeft;
    ref.current.style.transform = `translateX(${swipeAmt.current}px)`;
  };

  const onPointerUp = () => {
    if (startTime.current === null) return;
    const elapsed = Date.now() - startTime.current;
    const velocity = Math.abs(swipeAmt.current) / elapsed;

    if (Math.abs(swipeAmt.current) >= threshold || velocity > 0.11) {
      onDismiss();
    } else {
      ref.current.style.transition = 'transform 200ms ease-out';
      ref.current.style.transform = 'translateX(0)';
    }
    startTime.current = null;
  };

  return { ref, onPointerDown, onPointerMove, onPointerUp };
}

```

## Performance Considerations

The [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) file emphasizes that swipe animations must maintain 60fps performance. Animate only the **`transform`** and **`opacity`** properties during the drag operation, as these composite on the GPU without triggering layout recalculations.

Avoid updating CSS custom properties on ancestor elements during the gesture, as this forces style recalculation across the entire subtree. Instead, apply transforms directly to the target element. When the user releases the pointer, use a CSS transition to handle the snap-back animation or dismissal exit, ensuring the browser optimizes the property interpolation.

## Summary

- **Velocity calculation** divides absolute displacement by elapsed time to measure gesture speed in px/ms.
- **Dual-threshold logic** (`distance >= 30px OR velocity > 0.11`) captures both slow drags and quick flicks.
- **Implementation** requires tracking `Date.now()` timestamps alongside pointer coordinates in `pointerdown` and `pointerup` events.
- **Performance** depends on animating only `transform` and `opacity` properties to maintain compositor-only animations.
- **Reference implementation** resides in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) (lines 447-452) within the `emilkowalski/skills` repository.

## Frequently Asked Questions

### What is the optimal velocity threshold for swipe dismissal?

The **0.11 px/ms** threshold defined in the Emil Design Engineering guidelines provides a balanced sensitivity that catches intentional flicks while ignoring accidental bumps. This equates to roughly 110 pixels per second, a speed that distinguishes deliberate gestures from incidental contact. You may adjust this value based on your specific UI component size and user testing, but values below 0.08 px/ms tend to trigger too easily while values above 0.15 px/ms require uncomfortably fast movements.

### Why use velocity instead of just distance for swipe detection?

Distance-only detection creates a **false dichotomy** where users must choose between moving an element completely off-screen or abandoning the gesture. Velocity recognition respects the physics of real-world object manipulation—just as you can flick a playing card across a table without touching the far edge, users expect to "throw" digital elements away with similar brief, rapid gestures. This approach reduces interaction fatigue and feels more responsive, particularly for large modal drawers or fullscreen cards.

### How do I prevent velocity calculation errors on very short drags?

When `timeTaken` approaches zero (nearly instantaneous clicks), the velocity calculation `Math.abs(swipeAmount) / timeTaken` could theoretically produce Infinity or extremely high values. In practice, the distance threshold (`SWIPE_THRESHOLD`) acts as a safety gate—if the user barely moves the element (less than a few pixels), the distance check fails and the velocity check becomes irrelevant. For robust implementations, consider adding a minimum time guard (e.g., `if (timeTaken < 50) return`) to exclude accidental taps from velocity evaluation.

### Can velocity-based dismissal work with vertical swipes?

Yes, the pattern applies identically to **vertical orientations** by measuring `clientY` displacement instead of `clientX`. The velocity calculation remains `Math.abs(swipeAmount) / timeTaken`, though you may want distinct thresholds for horizontal versus vertical gestures depending on scroll behavior. In [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md), the pattern is described agnostically to axis—simply substitute the appropriate coordinate dimension in your `onPointerMove` handler and ensure your `SWIPE_THRESHOLD` accounts for the typical aspect ratio of your container.