npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-horizontal-scrolling-menu

v8.3.0

Published

Horizontal scrolling menu component for React with per-item visibility tracking. Supports mouse and touch devices, SSR-safe.

Downloads

325,826

Readme

React horizontal scrolling menu

npm NPM Downloads npm bundle size (minified + gzip) CI Available for hire

A horizontal scrolling menu component for React, built on native browser scrolling with per-item visibility tracking. Good for category rows, tab strips, chip filters, galleries — any row of things your app needs to reason about. Items are your own components with your own CSS; the menu is responsive to its parent width; navigation works by scrollbar, touch, mouse wheel, drag, or the arrow components you provide. 5.7 kB min+gzip.

example

Landing page · Live examples (Storybook, editable in the browser) · API

Used by

Over 20,000 repositories depend on this library. A few you can go and read:

Also featured in React Status #257.

Quick start

npm install react-horizontal-scrolling-menu
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 style={{ width: '160px' }} data-visible={isVisible}>
      {title}
    </div>
  );
}

Three things the example relies on:

  • Every item needs a unique itemId prop — that's how visibility 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.

Writing plain JavaScript? Drop the type imports and use React.useContext(VisibilityContext) as usual.

What it does — and doesn't

Built on native browser scrolling: momentum, scrollbar, touch, wheel and accessibility come from the browser, not a physics reimplementation. On top of that: per-item visibility via IntersectionObserver, scrollToItem / scrollNext / scrollPrev, an apiRef for control from outside, Header and Footer slots, RTL, dynamic add/remove detection, and TypeScript types throughout. SSR-safe — the landing page server-renders every demo.

No carousel engine: no snap or spring physics — if you want a fullscreen image slider, use Embla or Swiper. Autoplay and infinite loop aren't props either; they're recipes of about sixty lines each on the public API, live-editable in Storybook (infinite loop, autoplay). If you need a row that knows what's visible, this is it.

Examples

Every example is live-editable in the Storybook — each story ships with a Monaco editor loaded with the library's real type definitions. Covers: basic usage, one-item-per-scroll, mouse drag, scroll to item on mount, center on click, adding items dynamically, save/restore position, items animation, progress dots, preventing body scroll, custom transitions, infinite loop, autoplay, vertical layout, arrows in the footer, mobile swipe, RTL, and a 5000-item stress test.

AI agents

The package ships TanStack Intent skills — task-focused guidance for AI coding agents (correct v8 API, common silent failures, recipes), versioned with the library in skills/. If you use an agent, run npx @tanstack/intent@latest install to wire the skills into your setup.

Helpers and API

Children of the main ScrollMenu component (arrows, header, footer, items) can use VisibilityContext to access state and callbacks. Function callbacks also receive the context, e.g. onWheel, onScroll.

Properties and callbacks

| Prop | Signature | | ------------------------ | ----------------------------------------------------------------------------------------------------- | | LeftArrow | React component for left arrow | | RightArrow | React component for right arrow | | Header | React component Header | | Footer | React component Footer | | onWheel | (VisibilityContext, event) => void | | onScroll | (VisibilityContext, event) => void, fires before scroll settles | | onInit | (VisibilityContext) => void | | onUpdate | (VisibilityContext) => void | | apiRef | React.RefObject | React.RefCallback | | options | options for IntersectionObserver - rootMargin, threshold, and ratio to consider element visible | | containerRef | React.RefObject | React.RefCallback for the scroll container | | onMouseDown | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseLeave | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseUp | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseMove | (VisibilityContext) => (React.MouseEventHandler) => void | | onTouchMove | (VisibilityContext) => (React.TouchEventHandler) => void | | onTouchStart | (VisibilityContext) => (React.TouchEventHandler) => void | | onTouchEnd | (VisibilityContext) => (React.TouchEventHandler) => void | | itemClassName | ClassName of Item | | scrollContainerClassName | ClassName of scrollContainer | | wrapperClassName | ClassName of the outer-most div | | transitionDuration | Duration of transitions in ms, default 500, needs noPolyfill={false} | | transitionBehavior | 'smooth' | 'auto' | custom function, needs noPolyfill={false} | | RTL | Enable Right to left direction | | noPolyfill | true by default (native scrollIntoView); set false to enable transition props |

Note the two callback shapes: onWheel and onScroll are plain (context, event) => void, while the mouse and touch props are handler factories — (context) => (event) => void. See the MouseDrag story for the factory pattern in use.

VisibilityContext

Hooks (call them only inside components rendered under ScrollMenu, following the rules of hooks):

| Hook | Signature | | -------------------- | ------------------------------------------------------------------------ | | useIsVisible | (itemId: string | 'first' | 'last', defaultValue?: boolean) => boolean | | useLeftArrowVisible | () => boolean | | useRightArrowVisible | () => boolean |

Values and functions:

| Prop | Signature | | --------------------- | ------------------------------------------------------ | | getItemById | itemId => IOItem | undefined | | getItemElementById | itemId => DOM Element | null | | getItemByIndex | index => IOItem | undefined | | getItemElementByIndex | index => DOM Element | null | | getNextElement | () => IOItem | undefined | | getPrevElement | () => IOItem | undefined | | isFirstItemVisible | boolean | | isItemVisible | itemId => boolean | | isLastItem | boolean | | isLastItemVisible | boolean | | menuVisible | { current: boolean } | | scrollNext | (behavior, inline, block, ScrollOptions) => void | | scrollPrev | (behavior, inline, block, ScrollOptions) => void | | scrollToItem | (item, behavior, inline, block, ScrollOptions) => void | | items | ItemsMap class instance | | scrollContainer | Ref |

items class instance

ItemsMap stores info about all items, with methods to get currently visible items and the previous or next item. You can also subscribe to updates.

| Prop/method | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | subscribe | subscribe to events for itemId or first, last, onInit, onUpdate, e.g. items.subscribe('item5', (item) => setVisible(item.visible)) | | unsubscribe | use in useEffect for cleanup, pass the same callback instance | | getVisible | returns only visible items | | toItems | returns ids of all items | | toArr | returns all items | | first | returns the first item | | last | returns the last item | | prev | (itemId | Item) => previous item | undefined | | next | (itemId | Item) => next item | undefined |

Transitions and animation

transitionDuration and transitionBehavior ('smooth', 'auto', or a custom function) control how scrollToItem and the scroll helpers animate. Both require noPolyfill={false} — the default native scroll ignores them. They don't combine with the RTL prop.

See the CustomTransition story for a custom easing function.

ScrollOptions

The last argument of scrollToItem, scrollPrev and scrollNext overrides the transition props for that one call:

scrollToItem(getItemElementById('item-5'), 'smooth', 'center', 'nearest', {
  duration: 800, // milliseconds
});

Other helpers

slidingWindow

Get the previous or next group of visible items:

slidingWindow(allItems, visibleItems).prev();
// or .next()

getItemsPos

Get the first, center and last item of a group — e.g. to scroll to the center of the previous page:

const prevGroup = slidingWindow(allItems, visibleItems).prev();
const { center } = getItemsPos(prevGroup);
scrollToItem(getItemById(center), 'smooth', 'center');

apiRef

Pass a ref to ScrollMenu and the full VisibilityContext value is assigned to it — useful for firing functions like scrollToItem from outside the menu. Data values on the ref can go stale, so prefer calling functions:

apiRef.current.scrollToItem(apiRef.current.getItemElementById('item-3'));

You can also reach an item's DOM element directly via document.querySelector(`[data-key='${itemId}']`). See the ScrollToItem story and the AddItemAndScrollToIt story.

SSR

The library is SSR-safe: the first render emits plain markup and IntersectionObserver only attaches client-side. The useIsVisible defaultValue argument controls the server-rendered state — the canonical arrow pattern (('first', true) / ('last', false)) renders a disabled left arrow and enabled right arrow, matching a row scrolled to its start.

Next.js note

The package is ESM-first. On older Next.js setups you may hit “Cannot use import statement outside a module” — adding the package to transpilePackages resolves it.

Browser support

Requires IntersectionObserver and requestAnimationFrame — every modern browser. No IE.

Development

git clone https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu
cd react-horizontal-scrolling-menu
npm run setup
npm run demo        # example app (Next.js, port 3003) with the library in watch mode
npm run demo-tanstack  # example app (TanStack Start SSR, port 3004)
npm run storybook   # examples
npm test            # unit + e2e + storybook tests

Two integration example apps live in the repo — example-nextjs and example-tanstack (TanStack Start, server-rendered in workerd) — both rendering the same demo (mouse drag, body-scroll locking, custom animation with a control panel) so the one e2e suite in e2e/ runs against the library under both frameworks, including an assertion that the menu is already present in the server-rendered HTML.

Contributions and corrections are welcome — fork, commit, open a PR, and don't forget tests. See CONTRIBUTING and the CHANGELOG.

Docs for the legacy v1 API.

About

Built and maintained by Aleksandr Smyshliaev since 2018 — my first npm package, and still the same public API across React 16.8 to 19. I'm a frontend engineer (React / Next.js / TypeScript) and available for contract and full-time work.

A ⭐️ on the repo helps more people find the library.