Карусель отзывов на React, на нативной привязке прокрутки

Блок отзывов на любом маркетинговом сайте: одна карточка в центре, соседние наклонены позади неё, точки снизу, свайп или клик — к следующей. Слайдер-библиотеки продают ровно это. У браузера уже есть обе половины — scroll-snap-type для остановки и анимация по прокрутке для наклона, — а эта библиотека добавляет то, что не относится ни к одной из них: знание, какая карточка текущая.

Mia ChenProduct designer

Setup took an afternoon and the first report landed in my inbox the next morning. I have not opened the old spreadsheet since.

Tomás ReyesEngineering manager

The weekly digest is the only email my team reads end to end. Short, specific, and it names the person who unblocked you.

Aisha OkaforFounder

I wanted numbers I could defend in a board meeting. Every figure links back to the source, so nobody has to take my word for it.

Jonas LindqvistData analyst

Exports are plain CSV with sane column names. That alone saved me a script I had been maintaining for two years.

Priya NatarajanOperations lead

Support answered a Sunday question in twenty minutes with a fix, not a ticket number. That is what I pay for.

Lucas MoreauFreelance developer

The API does exactly what the docs say, and the docs are one page. I shipped the integration before lunch.

Свайпайте, перетаскивайте или пользуйтесь стрелками и точками — ряд каждый раз останавливается на карточке.

Для чего здесь на самом деле слайдер-библиотека

Карусель отзывов — каноническая установка Swiper: три видимые карточки, средняя плоская, точки пагинации, автовоспроизведение. Каждое из этого — либо CSS, либо несколько строк на API этой библиотеки:

Единственное, что надо сделать правильно: перетаскивание

Контейнер с обязательной привязкой привязывает каждую программную запись в scrollLeft, поэтому рецепт перетаскивания мышью как есть дёргался бы от карточки к карточке. Пример привязки прокрутки показывает решение: выключить привязку на время жеста, при отпускании плавно доехать до ближайшей карточки через scrollToItem и вернуть привязку CSS, когда движение успокоится, — включи раньше, и будет прыжок вместо плавного хода. Касанию ничего из этого не нужно: ряд — настоящий контейнер прокрутки.

Автовоспроизведение, цикл и что остаётся вашим

Автовоспроизведение — таймер, вызывающий тот же scrollToItem, с паузой при наведении, фокусе и уменьшенном движении. Бесконечный цикл тоже сочетается: телепорт сдвигает на длину цикла, то есть на целое число точек привязки. Карточки — ваши компоненты: аватар, оценка, цитата, логотип, — а геометрия — три пользовательских свойства: ширина карточки, отступ и наклон.

Паттерн в минимальном виде

Привязка и наклон — в таблице стилей; стрелки и точки — одно измерение и один scrollToItem. Демо выше — это плюс карточки.

TestimonialCarousel.tsx
function TestimonialCarousel({ reviews }: { reviews: Review[] }) {
  const [active, setActive] = React.useState(0);

  return (
    <ScrollMenu
      scrollContainerClassName="snap-rail"
      itemClassName="snap-slot"
      onScroll={(api) => setActive(nearestIndex(api, reviews))}
      LeftArrow={<Arrow id={reviews[active - 1]?.id} />}
      RightArrow={<Arrow id={reviews[active + 1]?.id} />}
    >
      {reviews.map((review) => (
        <ReviewCard itemId={review.id} key={review.id} {...review} />
      ))}
    </ScrollMenu>
  );
}

// The card whose center is closest to the rail's center.
function nearestIndex(api: publicApiType, reviews: Review[]) {
  const rail = api.scrollContainer.current!;
  const middle = rail.scrollLeft + rail.clientWidth / 2;
  const distances = reviews.map(({ id }) => {
    const slot = api.getItemElementById(id) as HTMLElement;
    return Math.abs(slot.offsetLeft + slot.offsetWidth / 2 - middle);
  });
  return distances.indexOf(Math.min(...distances));
}

// Arrows step one card: the neighbour of the centered one, centered.
function Arrow({ id }: { id?: string }) {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const step = () => {
    const el = id && api.getItemElementById(id);
    if (el) api.scrollToItem(el, 'smooth', 'center');
  };
  return <button disabled={!id} onClick={step}>→</button>;
}

/* .snap-rail — the browser lands every swipe on a card:
     scroll-snap-type: x mandatory;
     padding-inline: calc(50% - var(--card) / 2);
   .snap-slot — scroll-snap-align: center;
   The fan is the snap.css panel on the homepage. */

Или установите как компонент shadcn

Элемент реестра snap-carousel поставляет весь паттерн — привязку, веер, стрелки, точки и перетаскивание, укладывающееся на ближайшую карточку, — как компонент со стилями Tailwind в вашем components/ui/. Карточки приносите свои:

shadcn
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/snap-carousel.json

Похожие примеры

Все примеры (23)