Netflix-style horizontal row in React

The row of posters you scrub through on every streaming site is not a carousel — it is a menu. Nothing snaps, nothing autoplays; it rides native momentum scrolling with arrows overlaid on top. That is exactly what react-horizontal-scrolling-menu ships: your cards, native scroll, and per-item visibility so the arrows know when to hide.

Drag it, or hover the row — the arrows fade in over the edges, and each disappears when its end of the row is reached.

Why this is not a carousel job

A Netflix row never shows one slide at a time. Items are partially cut off at the edges on purpose — the cut-off poster is the affordance that says "there's more". Carousel engines fight this: they own the gesture layer with JavaScript transforms, snap to slide boundaries, and re-implement the momentum your users' browsers already have. On a row of clickable cards all of that is overhead.

Native scrolling gives you momentum, touch, trackpad and scrollbar for free. The two things it does not give you are the overlay arrows and knowing which cards are on screen — and those are the two things this library adds, via useIsVisible per item and edge-aware arrow state.

The three details that sell the effect

Scaling it: lazy rows and long rails

Streaming UIs stack dozens of rows with hundreds of cards. Because items are plain DOM in a native scroll container, nothing re-renders on scroll — the performance example runs 300 items without virtualization. Per-item visibility also gives you image lazy-loading for free: render a placeholder until useIsVisible reports the card on screen.

If your row should wrap around at the end, that is the one place slide semantics genuinely help — see the infinite loop recipe for the ~60-line userland version before reaching for a carousel engine.

The pattern, minimal

Overlay arrows over a native-scroll row — the demo above is this structure plus styling. Complete drop-in source, with drag and edge fade, ships as the shadcn component below.

MediaRow.tsx
function MediaRow({ children }: { children: React.ReactNode }) {
  return (
    <div className="group relative">
      <ScrollMenu Header={<OverlayArrows />} wrapperClassName="edge-fade">
        {children}
      </ScrollMenu>
    </div>
  );
}

// Rendered as Header, so it lives inside the menu's VisibilityContext
// and can sit absolutely positioned over the row ends.
function OverlayArrows() {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const atStart = api.useLeftArrowVisible();
  const atEnd = api.useRightArrowVisible();
  return (
    <>
      <button
        className="overlay-arrow left-0"
        hidden={atStart}
        onClick={() => api.scrollPrev()}
      >

      </button>
      <button
        className="overlay-arrow right-0"
        hidden={atEnd}
        onClick={() => api.scrollNext()}
      >

      </button>
    </>
  );
}

/* .edge-fade — the Netflix edge, one CSS line:
   mask-image: linear-gradient(to right,
     transparent, black 40px, black calc(100% - 40px), transparent); */

Or install it as a shadcn component

The media-row registry item is this exact pattern — hover arrows, gradient edge fade, drag to scroll — as a Tailwind-styled component in your components/ui/, yours to edit:

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

Related examples

All 21 examples