Drag Inertia Carousel
A rail you can throw — it projects the momentum and settles on the nearest slide.
- Category
- Patterns
- Added
- 2026-09-20
- Deps
- 1
- Updated
- 2026-09-20
Props
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
type Slide = { id: string; label: string };
export function DragInertiaCarousel({
slides,
slideWidth = 132,
power = 0.22,
stiffness = 260,
showDots = true,
}: {
slides: Slide[];
/** Card width + gap, in px. */
slideWidth?: number;
/** How far velocity carries the throw. Higher = flickier. */
power?: number;
stiffness?: number;
/** The pager dots under the rail, which also jump to a slide. */
showDots?: boolean;
}) {
const [index, setIndex] = useState(0);
const maxIndex = slides.length - 1;
return (
<div className="flex flex-col gap-3">
<div className="overflow-hidden">
<motion.div
drag="x"
dragConstraints={{ left: -slideWidth * maxIndex, right: 0 }}
dragElastic={0.12}
/*
* Framer's own momentum is OFF. We resolve the throw ourselves in
* onDragEnd; leaving both enabled makes the rail coast past the
* chosen slide and then spring back — the classic "fighting the
* carousel" feel.
*/
dragMomentum={false}
onDragEnd={(_, info) => {
/*
* Project where the throw WOULD land using the gesture's velocity,
* then round to the nearest slide. Snapping from the release
* OFFSET alone ignores how hard it was thrown, so a hard flick and
* a gentle nudge of the same distance behave identically — which
* is exactly what feels wrong in most drag carousels.
*/
const projected = info.offset.x + info.velocity.x * power;
const next = Math.round(index - projected / slideWidth);
setIndex(Math.max(0, Math.min(maxIndex, next)));
}}
animate={{ x: -index * slideWidth }}
transition={{ type: "spring", stiffness, damping: 32, mass: 0.8 }}
className="flex cursor-grab gap-3 active:cursor-grabbing"
>
{slides.map((slide) => (
<div
key={slide.id}
className="flex h-28 w-[120px] shrink-0 select-none flex-col justify-between rounded-md bg-neutral-800 p-3"
>
<span className="font-mono text-[9px] tracking-[0.16em] opacity-60">
{slide.id}
</span>
<span className="text-sm font-bold text-white">{slide.label}</span>
</div>
))}
</motion.div>
</div>
{showDots && (
<div className="flex justify-center gap-1.5">
{slides.map((s, i) => (
<button
key={s.id}
onClick={() => setIndex(i)}
aria-label={"Go to " + s.label}
aria-current={i === index}
className={
"h-1.5 rounded-full transition-all " +
(i === index ? "w-5 bg-blue-500" : "w-1.5 bg-white/25")
}
/>
))}
</div>
)}
</div>
);
}
About this pattern
Almost every drag carousel decides which slide to land on by looking at how far the rail moved, which means a hard flick and a slow nudge covering the same distance produce the same result — and that mismatch between effort and outcome is what makes them feel unresponsive. Projecting the throw fixes it in one line: add the release velocity, scaled by a tuning constant, to the drag offset before rounding to a slide index. A flick then carries several slides while a nudge advances one, which is what every native scroller does. The second requirement is turning Framer's own `dragMomentum` off. With both its inertia and a custom snap animation running, the rail coasts past the chosen slide under momentum and is then pulled back by the spring — the visible fight that makes a carousel feel like it disagrees with you. Clamping the projected index to the slide range is what keeps a hard throw from overshooting into empty space. Dots should be real buttons with `aria-current`, since dragging is a mouse and touch gesture and gives keyboard users no way through the content.
Curator’s note
Built for viberdy 2.0. Velocity projection is the whole difference between a carousel that feels thrown and one that feels dragged.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Swipe Card Stack
patternsA draggable deck of cards you flick away to cycle through, Tinder-style.
Infinite Drag Canvas
patternsAn endlessly pannable board that recycles a fixed set of tiles via modulo wrapping.
3D Card Carousel
patternsCards mounted on a rotating 3D ring — the active one faces forward, others recede.