“Setup took an afternoon and the first report landed in my inbox the next morning. I have not opened the old spreadsheet since.”
El menú horizontal que sabe qué es visible
Un menú de desplazamiento de React construido sobre el propio desplazamiento del navegador: seguimiento de visibilidad por elemento, flechas, arrastre y una API imperativa completa. 5,7 kB en gzip.
getVisible() → ['scifi', 'comedy', 'drama', 'horror', 'docs', 'kids']
This is the library, live — drag it. Dimmed tiles are the ones useIsVisible reports as off-screen.
Autoplay, como receta de sesenta líneas
No hay una prop autoplay — este carril es una receta sobre la API pública: la fila clonada en ambos extremos, un salto de scrollLeft en la costura y un temporizador que llama a scrollNext(). Se pausa al pasar el ratón, con el foco y en pestañas ocultas, se queda quieto bajo movimiento reducido — y puedes arrastrarlo, incluso hacia atrás, a través de la costura.
Un componente, cada fila horizontal
Embla, Swiper y keen-slider reimplementan el desplazamiento en JavaScript para construir sliders de imágenes: puntos de ajuste, física de muelle, un bucle de render. Esta biblioteca no trae nada de eso. Usa el desplazamiento nativo del navegador y añade lo único que el navegador no da: saber exactamente qué elementos hay en pantalla.
Filas de categorías, carruseles de tarjetas, franjas de pestañas, filtros de chips, galerías — cualquier fila de cosas sobre la que tu aplicación necesite razonar, construida con tus propios componentes y tu propio CSS. Lo único que se salta son los efectos de diapositiva vistosos; una biblioteca especializada en eso se prueba en minutos, y no hay ningún problema en recurrir a ella.
Desplazamiento nativo
La inercia, la barra de desplazamiento, el tacto, la rueda y la accesibilidad vienen del navegador, no de un motor de física. La fila se desplaza antes de que tu JavaScript hidrate: cada demo de esta página está renderizada en el servidor.
Seguimiento de visibilidad
IntersectionObserver informa de qué elementos hay en pantalla. useIsVisible(itemId) suscribe un componente a un elemento: sin matemática de posición de desplazamiento, y solo se vuelven a renderizar los elementos afectados.
Imperativo cuando lo necesitas
scrollToItem, scrollNext, scrollPrev, búsqueda por id o índice — a través del contexto dentro del menú, o de apiRef desde fuera.
Tus componentes, tu CSS
Las flechas, la cabecera, el pie y cada elemento son componentes que escribes tú. El ancho del elemento es tu CSS. La biblioteca trae 210 bytes de estilos de diseño y no se interpone.
Inicio rápido
Un archivo, sin configuración: elementos con itemId, dos flechas leyendo VisibilityContext y la importación de la hoja de estilos.
import React from 'react';
import {
ScrollMenu,
VisibilityContext,
type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';
const items = Array.from({ length: 10 }, (_, i) => `item-${i + 1}`);
export function App() {
return (
<ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((id) => (
<Card itemId={id} key={id} title={id} />
))}
</ScrollMenu>
);
}
function LeftArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isFirstVisible = visibility.useIsVisible('first', true);
return (
<button
disabled={isFirstVisible}
onClick={() => visibility.scrollPrev()}
>
←
</button>
);
}
function RightArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isLastVisible = visibility.useIsVisible('last', false);
return (
<button
disabled={isLastVisible}
onClick={() => visibility.scrollNext()}
>
→
</button>
);
}
function Card({ itemId, title }: { itemId: string; title: string }) {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isVisible = visibility.useIsVisible(itemId);
return (
<div className="card" data-visible={isVisible}>
<div>{title}</div>
<div>visible: {String(isVisible)}</div>
</div>
);
}The code on the left, running:
itemId es obligatorio en cada elemento: así funciona el seguimiento. La key de React funciona como respaldo.
styles.css es una importación aparte; el bundle JS nunca inyecta CSS.
El ancho del elemento viene de tu propio CSS: el menú no mide nada.
O pásaselo a tu agente de código
Los modelos entrenados con versiones antiguas siguen buscando visibleElements, elementos Separator y una prop Arrows — todo eliminado hace años — e inventan una prop autoplay que nunca existió. Para evitarlo, el paquete incluye ocho archivos SKILL.md: guías por tareas que tu agente carga bajo demanda a través de TanStack Intent, versionadas con la biblioteca en lugar de con esta página.
Ejecútalo una vez en un proyecto que ya tenga el paquete instalado. Tu agente descubre entonces las habilidades desde node_modules/react-horizontal-scrolling-menu/skills/.
menu-setupUn primer menú funcional, flechas, la importación CSS requeridamenu-visibilityQué hay en pantalla y el estado de las flechas en los extremosmenu-scrollingscrollToItem, apiRef, paginación página a páginamenu-interactionsArrastre, rueda y tacto — y sus fábricas de manejadoresmenu-recipesAutoplay, bucle infinito, cargar más: recetas, no propsmenu-transitions-rtlTiempo de animación, easing personalizado, derecha a izquierdamenu-testing-ssrNext.js y RSC, mocks de Jest, Playwrightmenu-migrationActualizar código anterior a v8 y las API que los modelos aún inventan
Leer las habilidades en GitHub llms.txt — los mismos hechos, condensados
Recetas que enviarás a producción
Cinco patrones comunes, en vivo, con las líneas que importan.
Una franja de pestañas que centra la pestaña activa
Haz clic en una pestaña: scrollToItem con inline: 'center' la lleva al medio de la fila. La misma llamada maneja start, end y la paginación.
function Tab({ itemId, label }: { itemId: string; label: string }) {
const api = React.useContext<publicApiType>(VisibilityContext);
const centerOnClick = () => {
const el = api.getItemElementById(itemId);
if (el) api.scrollToItem(el, 'smooth', 'center');
};
return <button onClick={centerOnClick}>{label}</button>;
}Añade un chip, desplázate a él
El estado vive fuera del menú; apiRef llega dentro. Añade un filtro y la fila lo sigue.
const apiRef = React.useRef<publicApiType>(null);
const lastAdded = React.useRef<string | null>(null);
function addChip(id: string) {
lastAdded.current = id;
setChips((current) => [...current, id]);
}
// After the new chip renders, scroll it into view from outside
// the menu — this is what apiRef is for.
React.useEffect(() => {
const id = lastAdded.current;
if (!id) return;
const el = apiRef.current?.getItemElementById(id);
if (el) apiRef.current?.scrollToItem(el, 'smooth', 'end');
lastAdded.current = null;
}, [chips]);
<ScrollMenu apiRef={apiRef}>…</ScrollMenu>Carga más cuando aparece el final
onUpdate te avisa cuando el último elemento se vuelve visible: añade la siguiente página justo ahí. Sin listeners de desplazamiento, sin umbrales de píxeles que afinar.
<ScrollMenu
onUpdate={(api) => {
// react in onUpdate, not onScroll — onScroll fires
// before the visibility state settles
if (api.items.last()?.visible) loadMore();
}}
>
{cards}
</ScrollMenu>De derecha a izquierda, con una prop
RTL invierte la dirección del contenedor de desplazamiento; las flechas y la lógica de paginación le siguen.
<ScrollMenu RTL LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((item) => (
<Item itemId={item.id} key={item.id} label={item.label} />
))}
</ScrollMenu>Tarjetas que se ajustan al centro y se abren en abanico
Dos funciones de CSS hacen el carrusel: scroll-snap-type hace aterrizar cada deslizamiento en una tarjeta, y una animación dirigida por el desplazamiento inclina cada tarjeta según su distancia al centro. La biblioteca te dice qué tarjeta es la actual.
/* Snap: the browser lands every swipe on a card */
.snap-rail {
scroll-snap-type: x mandatory;
padding-inline: calc(50% - var(--card) / 2);
}
.snap-slot {
scroll-snap-align: center;
}
/* Fan: each card's own position in the rail is the timeline */
.snap-card {
transform-origin: 50% 100%;
animation: snap-tilt linear both;
animation-timeline: view(inline);
animation-range: cover calc(50% - 2 * var(--pitch))
cover calc(50% + 2 * var(--pitch));
}
@keyframes snap-tilt {
from { transform: translateY(1.5rem) rotate(16deg) scale(0.88); }
50% { transform: none; }
to { transform: translateY(1.5rem) rotate(-16deg) scale(0.88); }
}Qué hay en la caja
- Hooks de visibilidad por elemento —
useIsVisible(itemId) - Helpers
first/lastpara el estado de las flechas scrollToItem·scrollNext·scrollPrevapiRefpara el control desde fuera del menú- Entrada por arrastre, rueda, tacto y barra de desplazamiento
- Detección dinámica de añadir/eliminar
- Ranuras Header y Footer
- Helpers de paginación
slidingWindow+getItemsPos - Soporte de derecha a izquierda
- Funciones de transición personalizadas
- Seguro para SSR — esta página lo demuestra
- TypeScript-first — se exporta
publicApiType - Una API estable de React 16.8 a 19
No está en la caja
- Física de ajuste y de muelle
- Sliders de imágenes a pantalla completa
- Lightboxes
Los efectos de diapositiva son un problema ya resuelto en otra parte — si una página necesita transiciones cube, usa una biblioteca que las incluya; probarla lleva minutos. El bucle infinito y el autoplay no son props: son recetas, de unas sesenta líneas de la API pública cada una, editables en vivo en Storybook. El carril cerca de la parte superior de esta página es exactamente esa receta en funcionamiento.
Descargado 347.516 veces el mes pasado por unos 20.000 repositorios — mantenido desde 2018.
Cada ejemplo es editable, en tu navegador
Storybook hace las veces de patio de juegos: cada historia viene con un editor Monaco cargado con las definiciones de tipo reales de la biblioteca. Cambia el código, mira cómo se vuelve a renderizar — sin cuenta de sandbox ni configuración local.
