SwipiDocumentation

Guides

Slides

How many slides sit in the viewport is a line of CSS — including three at a time, mixed widths and the snap count that follows.

How many slides are visible

Nothing about the number of visible slides passes through the hook. The slide's flex-basis decides it, the carousel measures the result and the snap positions follow.

Slide width
/* one slide at a time */
.carousel__slide { flex: 0 0 100%; }

/* three slides in the viewport */
.carousel__slide { flex: 0 0 calc(100% / 3); }

/* three slides and a peek of the fourth */
.carousel__slide { flex: 0 0 calc(100% / 3.4); }

/* a fixed width, however many fit */
.carousel__slide { flex: 0 0 280px; }

The 0 in flex: 0 0 … matters as much as the basis: without flex-shrink: 0 the slides are squeezed to fit the viewport and every width you set is ignored.

Three slides in the viewport

Three at a time is calc(100% / 3) on the slide — or basis-1/3 in Tailwind — with the gap paid for by the slide's padding and the track's negative margin. Here is the whole carousel, arrows included.

Markup
Styles
import { useSwipiCarousel } from '@midstem/swipi-react'

export const Carousel = ({ items }) => {
  const [carouselRef, carousel] = useSwipiCarousel()

  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/3 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>
    </>
  )
}

Slides of different widths

Because the widths are measured rather than declared, they do not have to agree with each other. A wider hero slide among narrower ones needs no option — it is a CSS selector, and the snap positions land where the slides actually start.

Mixed widths
.carousel__slide {
  box-sizing: border-box;
  flex: 0 0 calc(100% / 3);
  min-width: 0;
  padding-left: 16px;
}

/* every third slide is twice as wide */
.carousel__slide:nth-child(3n) {
  flex-basis: calc(100% / 1.5);
}

Slides, snaps and overflow

The number of snap positions follows from the measurements: the carousel stops once the remaining slides fit the viewport, so five half-width slides give four snaps rather than five. That is why dots are rendered from snapCount and not from the length of your array.

React
const [carouselRef, carousel] = useSwipiCarousel()

carousel.slidesCount // 5 — children found in the track
carousel.snapCount   // 4 — positions the carousel stops at
carousel.hasOverflow // true — there is more than fits
  • slidesCount — how many children the track has.
  • snapCount — how many positions the carousel rests at; the source for dots.
  • hasOverflow — whether dragging does anything at all. Hide the arrows and dots behind it and a short list stops looking like a carousel.

@ Copyright 2026 Midstem. All rights reserved.

DocsMidstemGitHubLinkedin