Gestures
A drag on a Riffle stack passes through four decisions before anything moves: whether the gesture belongs to the carousel at all, how far the front card tracks your pointer, what releasing means, and how much the card tilts along the way. This page covers all four, and ends with a live panel you can use to feel every option below on a running stack.
Slop and axis lock
Section titled “Slop and axis lock”A press doesn’t become a drag right away: Riffle waits for a small amount of movement, 6 pixels,
before it commits to anything. Below that, a pointerdown could still just be a tap, or the very
start of a page scroll.
Once the pointer clears those 6 pixels, Riffle decides, once, for the rest of that gesture, whether it belongs to the carousel or to the page:
- If the movement is mostly across the stack’s own axis rather than along it, the gesture is
abandoned: Riffle lets go, and the page’s own scrolling takes over instead. This is why a
horizontal stack (
axis: 'x', the default) never fights a vertical scroll, and a vertical stack (axis: 'y') never fights a horizontal one. - Otherwise the gesture locks: the stack starts tracking the pointer, and every further move drags it, regardless of which way the pointer wanders after that.
This decision is made exactly once, the instant those 6 pixels clear, and never changes mid-gesture: a drag can’t start as a scroll and then switch to dragging the stack, or the other way around.
A vertical stack
Section titled “A vertical stack”Pass axis: 'y' to createRiffle and the whole gesture turns a quarter: the stack drags up and
down, the page keeps its horizontal scroll, and the arrow keys become Up and Down. This stack of
tracks also passes bounds: 'clamp', so it stops at either end instead of looping, and a
fan({ offset: -32 }) layout defined once, at module scope:
const layout = fan({ offset: -32 })
const riffle: Riffle = createRiffle(stack, { count: tracks.length, axis: 'y', bounds: 'clamp', layout, cardWidth: width, cardHeight: ROW_HEIGHT, getLabel,})The commit rule
Section titled “The commit rule”While dragging, the drag distance is measured as a fraction of one step, called progress: 0 is
back where the drag started, 1 is a full step to the next card, -1 a full step to the previous
one. Releasing the pointer decides one of three things: commit forward, commit backward, or spring
back to where the drag began.
Two options control that decision:
threshold: the fraction of a step that commits on distance alone. Defaults to 0.25, a quarter of a step.flingVelocity: the release speed, in pixels per millisecond, that commits regardless of distance. Defaults to 0.5.
byFling = |velocity| >= flingVelocitybyDistance = |progress| >= threshold
no commit if neither is truecommit by velocity if byFlingcommit by distance otherwiseVelocity wins whenever it clears the fling bar. Dragging 40% of the way toward the next card
and then flicking back sharply commits in the direction of the flick, not the direction of the
larger on-screen movement, because the flick is the more recent, more deliberate signal. Only when
a release doesn’t clear flingVelocity does distance alone decide it, against threshold.
Velocity itself is read over a short, recent window (roughly the last 100 milliseconds), not averaged over the whole gesture, so a drag that stops dead before release always reads as no fling, however fast it was earlier on.
Rotation
Section titled “Rotation”While dragging, the front card tilts a little. rotation.maxRotation caps how far (default 16
degrees), or pass rotation: false to turn tilting off entirely without affecting the drag
itself. Three contributions add up to make the tilt, each scaled by that same cap:
- Distance (
rotation.baseFactor, default 0.35): the further the drag has travelled toward committing, the more the card tilts. - Grip (
rotation.leverFactor, default 0.65): where you grabbed the card changes which way it tilts. Grab it near one edge and it pivots one way; grab the opposite edge and the same drag pivots it the other way, the way pulling a physical card by a corner behaves differently from pulling it by its centre. - Trajectory (
rotation.trajFactor, default 0.15): a diagonal drag picks up a touch more tilt than a perfectly straight one at the same distance.
Try it
Section titled “Try it”Every slider below is one of these same options, live on a running stack: moving one passes the new
value to the engine’s update(), so each drag on the stack feels exactly like the number you set.
Live
“Copy config” writes the options that differ from their defaults to your clipboard, as a typed
TypeScript object ready to merge into your own createRiffle options.
Next: Layouts covers stepTravel(), the other half of what a drag feels like, and
walks through writing a custom LayoutStrategy.