SwipiDocumentation

Guides

Drag & momentum

What a gesture does by default, what dragFree changes and how the animation speed is spent.

What a gesture does

Pointer and drag listeners are attached to the viewport element directly, so nothing has to be spread onto your markup. A press that moves less than a few pixels is left alone and reaches whatever is inside the slide — a link stays clickable, a button stays pressable. Past that threshold the gesture becomes a drag and the track follows the pointer.

The axis is locked on the way in: a gesture that starts more vertical than horizontal is released back to the page, so a swipe down the screen scrolls it instead of nudging the carousel sideways — the other way round on a vertical carousel. Mouse, touch and pen all go through the same pointer events, so there is no separate touch mode to enable, and only the primary button starts a drag.

dragFree

By default one gesture moves by one slide at most, however far it was dragged — the carousel snaps to the neighbouring position when you let go. With dragFree the momentum of the drag is kept instead: the track coasts and rests wherever it stops, with no snap at the end.

React
// one gesture moves by one slide at most
const [carouselRef] = useSwipiCarousel()

// the track coasts and rests wherever it stops
const [freeRef] = useSwipiCarousel({ dragFree: true })
  • Snapping is the right default for slides you page through — a hero, a gallery, anything with dots.
  • dragFree suits dense rows of thumbnails or tags, where landing between two items is not a mistake.
  • Either way selectedIndex keeps reporting the nearest snap, so dots and arrows go on working.

The CSS a gesture needs

Two declarations make dragging feel right, and both are yours to write.

Gesture CSS
.carousel__viewport {
  overflow: hidden;
  touch-action: pan-y;
}

.carousel__track {
  cursor: grab;
  user-select: none;
}

.carousel__track:active {
  cursor: grabbing;
}
  • touch-action: pan-y on the viewport lets a vertical swipe scroll the page while a horizontal one drags the carousel. Without it the browser claims the gesture first.
  • user-select: none on the track stops a drag from selecting the text inside the slides.

How the movement is animated

Moves the carousel makes on its own — an arrow, a dot, scrollTo, an autoplay tick — run for animationSpeed milliseconds on an ease-out curve. A flick is different: its duration comes from how fast you were moving, so a quick gesture lands quickly and a lazy one glides.

React
const [carouselRef] = useSwipiCarousel({
  animationSpeed: 700 // ms, default 300
})

animationSpeed: 0 removes the movement entirely, which is what a fade-in carousel wants — the slides cross-fade in CSS while the track jumps. The same jump is what respectReducedMotion produces when the system asks for less motion.

@ Copyright 2026 Midstem. All rights reserved.

DocsMidstemGitHubLinkedin