SwipiDocumentation

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.

React
const [carouselRef, carousel] = useSwipiCarousel({ loop: true })
  • canScrollPrev and canScrollNext stay 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.

React
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.
Markup
Styles
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.

React
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>
)

@ Copyright 2026 Midstem. All rights reserved.

DocsMidstemGitHubLinkedin