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, sin motor de carrusel
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 menú, no un carrusel
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.
La herramienta equivocada para un slider de imágenes a pantalla completa: usa Embla o Swiper allí. La herramienta correcta para filas de categorías, pestañas, filtros de chips y cualquier fila de cosas sobre la que tu aplicación necesite razonar.
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
Cuatro 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>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
Eso pertenece al mundo de los sliders de imágenes — Embla y Swiper lo hacen bien. El bucle infinito y el autoplay tampoco 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. Esto sigue siendo un menú.
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.
