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.
Edit this example live in Storybook
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
- Arrows overlay the content, they don't sit beside it. Render them absolutely positioned over the row ends (the demo above passes them through
Headerso they stay inside the menu's context), show them on hover, and hide each one whenuseLeftArrowVisible/useRightArrowVisiblereport that end of the row is reached. - Edges fade. One line of CSS — a
mask-imagegradient on the scroll container — replaces the "peek" logic carousel plugins ship for this. - Drag must not fire clicks. A mouse drag that ends on a poster must not open it. The drag-to-scroll recipe tracks drag state and swallows exactly that click.
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.
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:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/media-row.json