A category rail for your store, in React

Category rails — the tappable row of departments over a storefront grid — are the highest-traffic scroll containers in e-commerce, and they are menus, not carousels: every tile is a link, nothing snaps, and half a tile peeking at the edge is what invites the scroll.

Drag the rail or use the arrows — they disable at the real ends of the row.

Why native scroll wins on a storefront

Storefront rails live above the fold on pages you fight for every Lighthouse point on. A carousel engine ships tens of kilobytes of gesture emulation to do what the browser does natively; this library is ≈5.7 kB min+gzip and leaves scrolling to the platform, so there is no hydration jank — the rail scrolls before your JavaScript loads, which also means it works in the server-rendered HTML your crawlers see. This page is itself server-rendered proof: the demo above scrolls with JavaScript disabled.

The comparison page has the full table against Swiper, Embla, keen-slider and react-slick.

Visibility tracking is a storefront feature

Per-item visibility sounds like an implementation detail until you map it to merchandising:

Fit it to your design system

Tiles are your components — image cards, circles, text pills — each carrying an itemId. Height and width come from your CSS; the menu imposes no dimensions. Step one item at a time like a product slider with one-item-scroll, show a scroll progress indicator, or ship it RTL for Arabic and Hebrew stores with the RTL example — the rail is composition, not configuration.

The pattern, minimal

Tiles with an itemId, arrows from the visibility hooks — the whole rail is under forty lines.

CategoryRail.tsx
function CategoryRail({ categories }: { categories: Category[] }) {
  return (
    <ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
      {categories.map((category) => (
        <CategoryTile itemId={category.id} key={category.id} {...category} />
      ))}
    </ScrollMenu>
  );
}

function CategoryTile({ itemId, name, image }: CategoryTileProps) {
  // Lazy images for free: placeholders until the tile is on screen.
  const visibility = React.useContext<publicApiType>(VisibilityContext);
  const isVisible = visibility.useIsVisible(itemId, false);
  return (
    <a href={`/category/${itemId}`} className="rail-tile">
      {isVisible ? <img src={image} alt="" /> : <div className="ph" />}
      <span>{name}</span>
    </a>
  );
}

function LeftArrow() {
  const visibility = React.useContext<publicApiType>(VisibilityContext);
  const atStart = visibility.useLeftArrowVisible();
  return (
    <button disabled={atStart} onClick={() => visibility.scrollPrev()}>

    </button>
  );
}

Or install it as a shadcn component

The base scroll-menu registry item is this rail — shadcn-styled arrows, drag to scroll, hidden scrollbar — installed into your components/ui/ and styled by your tokens:

shadcn
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/scroll-menu.json

Related examples

All 21 examples