The horizontal menu that knows what’s visible

A React scrolling menu built on the browser’s own scroll — per-item visibility tracking, arrows, drag, and a full imperative API. 5.7 kB gzipped.

Sci-Fi
Comedy
Drama
Horror
Docs
Kids
Action
Indie
Anime
Classics
Thriller
Reality

getVisible() → ['scifi', 'comedy', 'drama', 'horror', 'docs', 'kids']

This is the library, live — drag it. Dimmed tiles are the ones useIsVisible reports as off-screen.

npm install react-horizontal-scrolling-menu347k downloads/month5.7 kB min+gzipReact 16.8 – 19MIT
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/scroll-menu.jsonOr a ready-made shadcn/ui component — arrows, drag-to-scroll, styled

Autoplay, as a sixty-line recipe

There’s no autoplay prop — this rail is a recipe on the public API: the row cloned onto both ends, one scrollLeft jump at the seam, and a timer calling scrollNext(). It pauses on hover, focus and hidden tabs, sits still under reduced motion — and you can drag it, even backwards, across the seam.

Tokyo
Oslo
Lima
Cairo
Sydney
Quito
Seoul
Porto
Denver
Hanoi

One component, every horizontal row

Embla, Swiper and keen-slider re-implement scrolling in JavaScript to build image sliders — snap points, spring physics, a render loop. This library ships none of that. It rides native browser scrolling and adds the one thing the browser doesn’t give you: knowing exactly which items are on screen.

Category rows, card carousels, tab strips, chip filters, galleries — any row of things your app needs to reason about, built from your own components and your own CSS. The one thing it skips is fancy slide effects; a library that specializes in those is minutes away to try, and there is no problem in reaching for one.

Native scrolling

Momentum, scrollbar, touch, wheel and accessibility come from the browser, not a physics engine. The row scrolls before your JavaScript hydrates — every demo on this page is server-rendered.

Visibility tracking

IntersectionObserver reports which items are on screen. useIsVisible(itemId) subscribes one component to one item — no scroll-position math, and only the affected items re-render.

Imperative when you need it

scrollToItem, scrollNext, scrollPrev, lookup by id or index — through context inside the menu, or apiRef from outside it.

Your components, your CSS

Arrows, header, footer and every item are components you write. Item width is your CSS. The library ships 210 bytes of layout styles and stays out of the way.

Quick start

One file, no configuration: items with an itemId, two arrows reading VisibilityContext, and the stylesheet import.

App.tsx
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:

item-1
visible: true
item-2
visible: true
item-3
visible: true
item-4
visible: false
item-5
visible: false
item-6
visible: false
item-7
visible: false
item-8
visible: false
item-9
visible: false
item-10
visible: false

itemId is required on every item — it’s how tracking works. The React key works as a fallback.

styles.css is a separate import; the JS bundle never injects CSS.

Item width comes from your own CSS — the menu measures nothing.

Read the full getting-started example

Or hand it to your coding agent

Models trained on older releases still reach for visibleElements, Separator items and an Arrows prop — all removed years ago — and invent an autoplay prop that never existed. The package ships eight SKILL.md files to stop that: task-scoped guidance your agent loads on demand through TanStack Intent, versioned with the library instead of with this page.

npx @tanstack/intent@latest install

Run once in a project that already has the package installed. Your agent then discovers the skills from node_modules/react-horizontal-scrolling-menu/skills/.

  • menu-setupA first working menu, arrows, the required CSS import
  • menu-visibilityWhat’s on screen, and arrow state at the edges
  • menu-scrollingscrollToItem, apiRef, page-at-a-time paging
  • menu-interactionsDrag, wheel and touch — and their handler factories
  • menu-recipesAutoplay, infinite loop, load-more: recipes, not props
  • menu-transitions-rtlAnimation timing, custom easing, right-to-left
  • menu-testing-ssrNext.js and RSC, Jest mocks, Playwright
  • menu-migrationUpgrading pre-v8 code, and the APIs models still invent

What’s in the box

  • Per-item visibility hooks — useIsVisible(itemId)
  • first / last helpers for arrow state
  • scrollToItem · scrollNext · scrollPrev
  • apiRef for control from outside the menu
  • Drag, wheel, touch and scrollbar input
  • Dynamic add/remove detection
  • Header and Footer slots
  • slidingWindow + getItemsPos paging helpers
  • Right-to-left support
  • Custom transition functions
  • SSR-safe — this page proves it
  • TypeScript-first — publicApiType exported
  • One stable API across React 16.8 – 19

Not in the box

  • Snap and spring physics
  • Fullscreen image sliders
  • Lightboxes

Slide effects are a solved problem elsewhere — if a page needs cube transitions, grab a library that ships them; trying one takes minutes. Infinite loop and autoplay aren’t props — they’re recipes: about sixty lines of the public API each, live-editable in Storybook. The rail near the top of this page is exactly that recipe, running.

Downloaded 347,516 times last month by some 20,000 repositories — maintained since 2018.

788 stars on GitHubFeatured in React Status #257In production at Our World in Data

Every example is editable, in your browser

The Storybook doubles as a playground: each story ships with a Monaco editor loaded with the library’s real type definitions. Change the code, watch it re-render — no sandbox account, no local setup.

Built and maintained by Aleksandr Smyshliaev

First published in 2018, same public API across React 16.8 to 19. Aleksandr is a frontend engineer — React, Next.js, TypeScript — currently open to contract and full-time work.