O menu horizontal que sabe o que está visível
Um menu de rolagem do React construído sobre a própria rolagem do navegador — rastreamento de visibilidade por item, setas, arraste e uma API imperativa completa. 5,7 kB 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, sem motor de carrossel
Não existe uma prop autoplay — este carril é uma receita sobre a API pública: a linha clonada nas duas pontas, um salto de scrollLeft na emenda e um temporizador chamando scrollNext(). Ele pausa no hover, no foco e em abas ocultas, fica parado sob movimento reduzido — e você pode arrastá-lo, até para trás, através da emenda.
Um menu, não um carrossel
Embla, Swiper e keen-slider reimplementam a rolagem em JavaScript para construir sliders de imagens — pontos de snap, física de mola, um loop de render. Esta biblioteca não entrega nada disso. Ela usa a rolagem nativa do navegador e adiciona a única coisa que o navegador não dá: saber exatamente quais itens estão na tela.
A ferramenta errada para um slider de imagens em tela cheia — use Embla ou Swiper lá. A ferramenta certa para linhas de categorias, faixas de abas, filtros de chips e qualquer linha de coisas sobre a qual seu app precise raciocinar.
Rolagem nativa
Inércia, barra de rolagem, toque, roda e acessibilidade vêm do navegador, não de um motor de física. A linha rola antes de seu JavaScript hidratar — cada demo desta página é renderizada no servidor.
Rastreamento de visibilidade
O IntersectionObserver informa quais itens estão na tela. useIsVisible(itemId) inscreve um componente em um item — sem matemática de posição de rolagem, e só os itens afetados são re-renderizados.
Imperativo quando você precisa
scrollToItem, scrollNext, scrollPrev, busca por id ou índice — pelo contexto dentro do menu, ou pelo apiRef de fora dele.
Seus componentes, seu CSS
Setas, header, footer e cada item são componentes que você escreve. A largura do item é seu CSS. A biblioteca entrega 210 bytes de estilos de layout e sai do caminho.
Início rápido
Um arquivo, sem configuração: itens com itemId, duas setas lendo VisibilityContext e a importação da folha 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 é obrigatório em cada item — é assim que o rastreamento funciona. A key do React funciona como fallback.
styles.css é uma importação separada; o bundle JS nunca injeta CSS.
A largura do item vem do seu próprio CSS — o menu não mede nada.
Ou entregue ao seu agente de código
Modelos treinados em versões antigas ainda procuram visibleElements, itens Separator e uma prop Arrows — tudo removido há anos — e inventam uma prop autoplay que nunca existiu. Para impedir isso, o pacote envia oito arquivos SKILL.md: orientação por tarefa que seu agente carrega sob demanda via TanStack Intent, versionada com a biblioteca em vez de com esta página.
Execute uma vez em um projeto que já tenha o pacote instalado. Seu agente então descobre as habilidades em node_modules/react-horizontal-scrolling-menu/skills/.
menu-setupUm primeiro menu funcional, setas, a importação CSS necessáriamenu-visibilityO que está na tela e o estado das setas nas bordasmenu-scrollingscrollToItem, apiRef, paginação página a páginamenu-interactionsArraste, roda e toque — e suas fábricas de handlersmenu-recipesAutoplay, loop infinito, carregar mais: receitas, não propsmenu-transitions-rtlTempo de animação, easing personalizado, direita para esquerdamenu-testing-ssrNext.js e RSC, mocks do Jest, Playwrightmenu-migrationAtualizar código anterior à v8 e as APIs que os modelos ainda inventam
Ler as habilidades no GitHub llms.txt — os mesmos fatos, condensados
Receitas que você realmente enviará
Quatro padrões comuns, ao vivo, com as linhas que importam.
Uma faixa de abas que centraliza a aba ativa
Clique em uma aba: scrollToItem com inline: 'center' a traz para o meio da linha. A mesma chamada lida com start, end e paginação.
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>;
}Adicione um chip, role até ele
O estado vive fora do menu; o apiRef alcança dentro. Adicione um filtro e a linha o segue.
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>Carregue mais quando o fim aparecer
onUpdate avisa quando o último item fica visível — acrescente a próxima página ali mesmo. Sem listeners de rolagem, sem limiares de pixel para ajustar.
<ScrollMenu
onUpdate={(api) => {
// react in onUpdate, not onScroll — onScroll fires
// before the visibility state settles
if (api.items.last()?.visible) loadMore();
}}
>
{cards}
</ScrollMenu>Da direita para a esquerda, com uma prop
RTL inverte a direção do contêiner de rolagem; as setas e a lógica de paginação seguem.
<ScrollMenu RTL LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((item) => (
<Item itemId={item.id} key={item.id} label={item.label} />
))}
</ScrollMenu>O que vem na caixa
- Hooks de visibilidade por item —
useIsVisible(itemId) - Helpers
first/lastpara o estado das setas scrollToItem·scrollNext·scrollPrevapiRefpara controle de fora do menu- Entrada por arraste, roda, toque e barra de rolagem
- Detecção dinâmica de adicionar/remover
- Slots Header e Footer
- Helpers de paginação
slidingWindow+getItemsPos - Suporte a direita para esquerda
- Funções de transição personalizadas
- Seguro para SSR — esta página prova
- TypeScript-first —
publicApiTypeexportado - Uma API estável do React 16.8 ao 19
Não vem na caixa
- Física de snap e de mola
- Sliders de imagens em tela cheia
- Lightboxes
Isso pertence ao mundo dos sliders de imagens — Embla e Swiper fazem bem. O loop infinito e o autoplay também não são props — são receitas: cerca de sessenta linhas da API pública cada, editáveis ao vivo no Storybook. O carril perto do topo desta página é exatamente essa receita em execução. Isto continua sendo um menu.
Baixado 347.516 vezes no mês passado por cerca de 20.000 repositórios — mantido desde 2018.
Cada exemplo é editável, no seu navegador
O Storybook funciona como um playground: cada história vem com um editor Monaco carregado com as definições de tipo reais da biblioteca. Mude o código, veja-o re-renderizar — sem conta de sandbox, sem configuração local.
