Skip to content

Drag Inertia Carousel

A rail you can throw — it projects the momentum and settles on the nearest slide.

Freeplayfulminimalglass
Category
Patterns
Added
2026-09-20
Deps
1
Updated
2026-09-20
Preview
01Measure
02Throw
03Decay
04Settle

Props

0.22
260
Show dots
Requires framer-motion
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured

A draggable deck of cards you flick away to cycle through, Tinder-style.

dragswipestack
Featured
NewPro

An endlessly pannable board that recycles a fixed set of tiles via modulo wrapping.

draginfinitecanvas

Cards mounted on a rotating 3D ring — the active one faces forward, others recede.

carousel3drotate