Un carrusel de testimonios en React, sobre scroll snap nativo

La sección de reseñas de todo sitio de marketing: una tarjeta en el centro, sus vecinas inclinadas detrás, puntos debajo, un deslizamiento o un clic para pasar a la siguiente. Las bibliotecas de slider venden exactamente esto. El navegador ya tiene las dos mitades — scroll-snap-type para el aterrizaje y una animación dirigida por el desplazamiento para la inclinación — y esta biblioteca añade la parte que no es ninguna de las dos: saber qué tarjeta es la actual.

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.

Desliza, arrastra o usa las flechas y los puntos — la fila se asienta sobre una tarjeta cada vez.

Para qué sirve realmente aquí una biblioteca de slider

Los carruseles de testimonios son la instalación canónica de Swiper: tres tarjetas visibles, la del medio plana, puntos de paginación, autoplay. Cada una de esas cosas es CSS o unas pocas líneas sobre la API de esta biblioteca:

Lo único que hay que hacer bien: el arrastre

Un contenedor con ajuste obligatorio ajusta cada escritura programática en scrollLeft, así que la receta de arrastre con el ratón tal cual iría a trompicones de tarjeta en tarjeta. El ejemplo de scroll snap muestra la solución: apagar el ajuste durante el gesto, deslizarse hasta la tarjeta más cercana con scrollToItem al soltar y devolver el ajuste a CSS cuando ese deslizamiento se haya asentado — reactivarlo antes salta en lugar de deslizarse. El tacto no necesita nada de esto; la fila es un contenedor de scroll real.

Autoplay, bucle y lo que sigue siendo tuyo

El autoplay es un temporizador que llama al mismo scrollToItem, en pausa al pasar el ratón, con el foco y con movimiento reducido. Un bucle infinito también se combina: su teletransporte se mueve una longitud de bucle entera, que es un número entero de puntos de ajuste. Las tarjetas son tus componentes — un avatar, una valoración, una cita, un logo — y la geometría son tres propiedades personalizadas: ancho de tarjeta, separación e inclinación.

El patrón, en su forma mínima

El ajuste y la inclinación son la hoja de estilos; las flechas y los puntos son una medición y un scrollToItem. La demo de arriba es esto más las tarjetas.

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. */

O instálalo como componente shadcn

El elemento de registro snap-carousel trae el patrón completo — ajuste, abanico, flechas, puntos y un arrastre que cae sobre la tarjeta más cercana — como componente con estilos Tailwind en tu components/ui/. Pon tus propias tarjetas:

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

Ejemplos relacionados

Todos los ejemplos (23)