---
title: "React category rail for e-commerce"
description: "A horizontal category rail in React: native scrolling, arrows that disable at the edges, per-item visibility for lazy images and analytics. Demo and source."
canonical: "https://react-horizontal-scrolling-menu.dev/category-rail"
image: "https://react-horizontal-scrolling-menu.dev/og.png"
---

# 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.

TTokyo

OOslo

LLima

CCairo

SSydney

QQuito

SSeoul

PPorto

DDenver

HHanoi

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

[Edit this example live in Storybook](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-simple--simple)

## 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](https://react-horizontal-scrolling-menu.dev/compare.md) 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:

-   **Lazy images** — render a placeholder tile until `useIsVisible` reports it on screen.
-   **Impression analytics** — `getVisible()` (live in the [hero demo](https://react-horizontal-scrolling-menu.dev/index.md) on the homepage) tells you exactly which categories were seen, not just that the rail rendered.
-   **Edge-aware arrows** — disable or hide at the true ends, even after categories load in async, as in the [add-items example](https://react-horizontal-scrolling-menu.dev/examples/add-items.md).

## 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](https://react-horizontal-scrolling-menu.dev/examples/one-item-scroll.md), show a scroll [progress indicator](https://react-horizontal-scrolling-menu.dev/examples/progress.md), or ship it RTL for Arabic and Hebrew stores with the [RTL example](https://react-horizontal-scrolling-menu.dev/examples/rtl.md) — 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](https://react-horizontal-scrolling-menu.dev/r/scroll-menu.json) 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

-   [Getting startedThe minimal menu: items, two arrows, visibility out of the box.](https://react-horizontal-scrolling-menu.dev/examples/simple.md)
-   [Scroll one item at a timeArrows advance a single item instead of a full page.](https://react-horizontal-scrolling-menu.dev/examples/one-item-scroll.md)
-   [Scroll progress indicatorA progress bar driven by which items are visible.](https://react-horizontal-scrolling-menu.dev/examples/progress.md)

[All 21 examples](https://react-horizontal-scrolling-menu.dev/examples.md)

---

More examples: <https://react-horizontal-scrolling-menu.dev/examples.md>
Library summary for LLMs: <https://react-horizontal-scrolling-menu.dev/llms.txt>
