Le menu horizontal qui sait ce qui est visible
Un menu à défilement React construit sur le propre défilement du navigateur — suivi de visibilité par élément, flèches, glisser et une API impérative complète. 5,7 kB gzippé.
getVisible() → ['scifi', 'comedy', 'drama', 'horror', 'docs', 'kids']
This is the library, live — drag it. Dimmed tiles are the ones useIsVisible reports as off-screen.
Lecture automatique, sans moteur de carrousel
Il n’y a pas de prop autoplay — ce rail est une recette sur l’API publique : la rangée clonée aux deux extrémités, un saut de scrollLeft à la couture et un minuteur appelant scrollNext(). Il se met en pause au survol, au focus et sur les onglets cachés, reste immobile sous mouvement réduit — et vous pouvez le faire glisser, même en arrière, à travers la couture.
Un menu, pas un carrousel
Embla, Swiper et keen-slider réimplémentent le défilement en JavaScript pour construire des sliders d’images : points d’accroche, physique de ressort, boucle de rendu. Cette bibliothèque ne fournit rien de tout cela. Elle s’appuie sur le défilement natif du navigateur et ajoute la seule chose que le navigateur ne donne pas : savoir exactement quels éléments sont à l’écran.
Le mauvais outil pour un slider d’images plein écran — utilisez Embla ou Swiper là-bas. Le bon outil pour les rangées de catégories, les onglets, les filtres à puces et toute rangée d’éléments dont votre application a besoin de raisonner.
Défilement natif
L’inertie, la barre de défilement, le toucher, la molette et l’accessibilité viennent du navigateur, pas d’un moteur physique. La rangée défile avant que votre JavaScript ne s’hydrate — chaque démo de cette page est rendue côté serveur.
Suivi de visibilité
IntersectionObserver signale quels éléments sont à l’écran. useIsVisible(itemId) abonne un composant à un élément — aucun calcul de position de défilement, et seuls les éléments concernés sont re-rendus.
Impératif quand vous en avez besoin
scrollToItem, scrollNext, scrollPrev, recherche par id ou index — via le contexte dans le menu, ou apiRef depuis l’extérieur.
Vos composants, votre CSS
Les flèches, l’en-tête, le pied de page et chaque élément sont des composants que vous écrivez. La largeur de l’élément est votre CSS. La bibliothèque fournit 210 octets de styles de mise en page et s’écarte du chemin.
Démarrage rapide
Un fichier, aucune configuration : des éléments avec itemId, deux flèches lisant VisibilityContext et l’import de la feuille de styles.
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 est requis sur chaque élément — c’est ainsi que fonctionne le suivi. La key de React sert de solution de repli.
styles.css est un import séparé ; le bundle JS n’injecte jamais de CSS.
La largeur de l’élément vient de votre propre CSS — le menu ne mesure rien.
Ou confiez-le à votre agent de code
Les modèles entraînés sur d’anciennes versions recherchent encore visibleElements, les éléments Separator et une prop Arrows — tous supprimés il y a des années — et inventent une prop autoplay qui n’a jamais existé. Pour mettre fin à cela, le paquet embarque huit fichiers SKILL.md : des guides par tâche que votre agent charge à la demande via TanStack Intent, versionnés avec la bibliothèque plutôt qu’avec cette page.
Exécutez une fois dans un projet où le paquet est déjà installé. Votre agent découvre ensuite les compétences depuis node_modules/react-horizontal-scrolling-menu/skills/.
menu-setupUn premier menu fonctionnel, des flèches, l’import CSS requismenu-visibilityCe qui est à l’écran et l’état des flèches aux extrémitésmenu-scrollingscrollToItem, apiRef, pagination page par pagemenu-interactionsGlisser, molette et toucher — et leurs fabriques de gestionnairesmenu-recipesLecture auto, boucle infinie, charger plus : des recettes, pas des propsmenu-transitions-rtlRythme d’animation, easing personnalisé, droite à gauchemenu-testing-ssrNext.js et RSC, mocks Jest, Playwrightmenu-migrationMettre à niveau le code antérieur à v8 et les API que les modèles inventent encore
Lire les compétences sur GitHub llms.txt — les mêmes faits, condensés
Des recettes que vous expédierez vraiment
Quatre motifs courants, en direct, avec les lignes qui comptent.
Une bande d’onglets qui centre l’onglet actif
Cliquez sur un onglet : scrollToItem avec inline: 'center' l’amène au milieu de la rangée. Le même appel gère start, end et la pagination.
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>;
}Ajoutez une puce, faites défiler vers elle
L’état vit hors du menu ; apiRef y accède. Ajoutez un filtre et la rangée le suit.
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>Charger plus quand la fin apparaît
onUpdate vous dit quand le dernier élément devient visible — ajoutez la page suivante juste là. Pas d’écouteurs de défilement, pas de seuils en pixels à régler.
<ScrollMenu
onUpdate={(api) => {
// react in onUpdate, not onScroll — onScroll fires
// before the visibility state settles
if (api.items.last()?.visible) loadMore();
}}
>
{cards}
</ScrollMenu>De droite à gauche, avec une prop
RTL inverse la direction du conteneur de défilement ; les flèches et la logique de pagination suivent.
<ScrollMenu RTL LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((item) => (
<Item itemId={item.id} key={item.id} label={item.label} />
))}
</ScrollMenu>Ce qu’il y a dans la boîte
- Hooks de visibilité par élément —
useIsVisible(itemId) - Helpers
first/lastpour l’état des flèches scrollToItem·scrollNext·scrollPrevapiRefpour le contrôle hors du menu- Entrée par glisser, molette, toucher et barre de défilement
- Détection dynamique d’ajout/suppression
- Emplacements Header et Footer
- Helpers de pagination
slidingWindow+getItemsPos - Prise en charge de droite à gauche
- Fonctions de transition personnalisées
- Sûr pour le SSR — cette page le prouve
- TypeScript-first —
publicApiTypeexporté - Une API stable de React 16.8 à 19
Pas dans la boîte
- Physique d’accroche et de ressort
- Sliders d’images plein écran
- Lightboxes
Cela appartient au monde des sliders d’images — Embla et Swiper le font bien. La boucle infinie et la lecture automatique ne sont pas non plus des props — ce sont des recettes : environ soixante lignes de l’API publique chacune, modifiables en direct dans Storybook. Le rail en haut de cette page est exactement cette recette en cours d’exécution. Cela reste un menu.
Téléchargé 347 516 fois le mois dernier par environ 20 000 dépôts — maintenu depuis 2018.
Chaque exemple est modifiable, dans votre navigateur
Storybook fait office de terrain de jeu : chaque histoire est livrée avec un éditeur Monaco chargé des vraies définitions de types de la bibliothèque. Changez le code, regardez-le se re-rendre — sans compte sandbox ni configuration locale.
