Guides
Autoplay & loop
An infinite carousel that moves the real slides around, and autoplay with a speed of your own.
loop
loop makes the carousel infinite. The real slides are moved around rather than cloned, so every slide stays a single DOM node: no duplicated ids, no images fetched twice and no surprises for anything that queries the track.
const [carouselRef, carousel] = useSwipiCarousel({ loop: true })canScrollPrevandcanScrollNextstay true, so arrows bound to them never disable.- Looping needs something to loop: with fewer slides than fit the viewport there is no overflow, and the carousel behaves as a static row.
- Uniform spacing is what keeps a lap seamless — the slide padding with the track's negative margin, never a trailing padding. See Spacing & sizing.
autoplay
autoplay advances the carousel on its own every autoplaySpeed milliseconds — 4000 by default — while animationSpeed decides how long each of those moves takes.
const [carouselRef, carousel] = useSwipiCarousel({
loop: true,
autoplay: true,
autoplaySpeed: 3500,
animationSpeed: 700
})- The timer restarts whenever the selected index changes, so dragging or pressing an arrow gives you a full interval before the next automatic move.
- Ticks are skipped while the tab is hidden, so a carousel left in a background tab does not race through its slides when you come back.
- Without
loop, autoplay stops at the last snap: there is nothing left to advance to.
import { useSwipiCarousel } from '@midstem/swipi-react'
export const Carousel = ({ items }) => {
const [carouselRef, carousel] = useSwipiCarousel({ loop: true, autoplay: true, autoplaySpeed: 3000 })
return (
<>
<div className="overflow-hidden touch-pan-y" ref={carouselRef}>
<div className="flex -ml-4 cursor-grab select-none active:cursor-grabbing">
{items.map((item) => (
<div className="min-w-0 shrink-0 grow-0 basis-1/2 pl-4" key={item.id}>
{item.title}
</div>
))}
</div>
</div>
<button
type="button"
className="z-10 bg-transparent border-none cursor-pointer disabled:opacity-[0.35] disabled:cursor-default"
onClick={carousel.scrollPrev}
disabled={!carousel.canScrollPrev}
>
‹
</button>
<button
type="button"
className="z-10 bg-transparent border-none cursor-pointer disabled:opacity-[0.35] disabled:cursor-default"
onClick={carousel.scrollNext}
disabled={!carousel.canScrollNext}
>
›
</button>
<nav className="relative flex items-center gap-2.5">
{Array.from({ length: carousel.snapCount }, (_, index) => (
<button
type="button"
className="h-3 w-3 p-0 bg-[#d2d2d2] border-none rounded-full cursor-pointer data-[active=true]:bg-[#b70808]"
key={index}
data-active={index === carousel.selectedIndex}
onClick={() => carousel.scrollTo(index)}
/>
))}
</nav>
</>
)
}Pausing
autoplay is a plain boolean, so pausing is a piece of state you own. Hand it a false and the timer is cleared; hand it a true and it starts again from a full interval. Options are read on every render, so a piece of state is all it takes.
const [isPlaying, setIsPlaying] = useState(true)
const [carouselRef, carousel] = useSwipiCarousel({
loop: true,
autoplay: isPlaying
})
return (
<div
onMouseEnter={() => setIsPlaying(false)}
onMouseLeave={() => setIsPlaying(true)}
>
<div className="carousel__viewport" ref={carouselRef}>
{/* … */}
</div>
<button type="button" onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)