“Setup took an afternoon and the first report landed in my inbox the next morning. I have not opened the old spreadsheet since.”
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.
Desliza, arrastra o usa las flechas y los puntos — la fila se asienta sobre una tarjeta cada vez.
Edita este ejemplo en vivo en Storybook
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:
- Aterrizar en una tarjeta es
scroll-snap-type: x mandatoryen la fila yscroll-snap-align: centeren los elementos. El desplazamiento táctil, con rueda y con teclado sigue siendo nativo y desacelera solo hasta una tarjeta. - La inclinación es una animación dirigida por el desplazamiento en cada tarjeta, así que el ángulo sigue la posición de scroll píxel a píxel sin medir nada en JavaScript. Chromium y Safari 26 la dibujan; Firefox aún la mantiene tras una bandera y muestra tarjetas planas, y la regla está protegida con
@supports. - Puntos y flechas necesitan saber la tarjeta actual:
onScrollencuentra la más cercana al centro de la fila, yscrollToItem(el, 'smooth', 'center')avanza a una vecina o salta a la tarjeta de un punto, cayendo en un punto de ajuste por construcción.
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.
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:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/snap-carousel.json