v2.4 — sheet snapping is in

The gesture code you keep
rewriting, already correct.

Nine React hooks for drag, scroll and press. Interruptible springs, ease-out on everything that enters, nothing over 300 ms, and reduced-motion handled before you ask. 9 kB for the whole set.

$ npm i ripplekit
See it move

React 18+ · zero peer deps · MIT

Three of them, running right here.

Not a video. These are the shipped hooks, on this page, at their default settings.

Drag to dismiss

useDragDismiss

Build finished

Flick it either way. Velocity dismisses it, not distance.

Distance OR velocity. Threshold-only dismissal is why so many cards feel stuck.

Scroll progress

useScrollProgress
0percent

The value is sprung, so it trails your scroll by a few frames instead of snapping to it.

Magnetic pointer

useMagnetic

Spring interpolation, not a raw mouse offset. Raw offsets have no momentum and read as artificial.

Nine hooks. Six worth naming.

Each one is independently importable — import two, ship two. The diagram on each card is the decision the hook makes for you.

useDragDismiss1.4 kB

Swipe a card away. Reads velocity, so a flick counts even when the distance does not.

useScrollProgress0.9 kB

Element progress through the viewport, spring-smoothed. Reads off a passive scroll listener.

useMagnetic0.8 kB

Pointer attraction with spring falloff. Ignores touch and coarse pointers on its own.

useSheet2.1 kB

Snap points, damping past the last one, and a drag that hands off to momentum instead of stopping dead.

usePressScale0.4 kB

scale(0.97) on press, 160 ms, correct curve. Four hundred bytes to stop shipping dead buttons.

useInterruptible1.2 kB

Retargets a running animation from its current velocity rather than restarting it from zero.

What a dismissable card costs you.

By hand

const [x, setX] = useState(0);
const start = useRef(0);
const t0 = useRef(0);

onPointerDown  → capture pointer, store x + time
onPointerMove  → setX(e.clientX - start.current)
onPointerUp    → const dt = Date.now() - t0.current;
                 const v  = Math.abs(x) / dt;
                 if (Math.abs(x) > 90 || v > 0.11) dismiss();
                 else animate back — but from what velocity?

// then: multi-touch guard, pointer capture on leave,
// damping past the edge, reduced motion, and the
// spring that has to retarget mid-flight instead of
// restarting from zero.

With Ripple Kit

import { useDragDismiss } from "ripplekit";

const card = useDragDismiss({
  onDismiss: () => remove(id),
});

return <div {...card} className="card" />;

Same behaviour, including the parts you would have found in review: the flick threshold, the boundary damping, the pointer capture, and an exit that stays faster than the entrance.

Stop shipping motion you had to guess at.

One install, nine hooks, and the defaults are the part you were going to get wrong.

$ npm i ripplekit
Read the source