Nested Row Carousels — HTML CSS JS Snippet

Nested Row Carousels · Carousels · Plain HTML, CSS & JS · Live preview

What's included

Features

Native horizontal scrolling per row — no JS translate math, drag handling, or snap logic to maintain
One reusable wiring function applied to every .nrc-row via querySelectorAll, however many exist
Each row's scroll state, tiles, and arrows are fully isolated from every other row
Arrow buttons fade in on hover and auto-hide at either true scroll boundary
Free momentum, bounds, and touch scrolling for every row, inherited from the browser
Scrollbar hidden cross-browser while remaining fully scrollable by drag, wheel, or trackpad

About this UI Snippet

Nested Row Carousels — One Reusable Pattern Applied to Every Row

Screenshot of the Nested Row Carousels snippet rendered live

Streaming apps don't show one carousel — they show a *stack* of them, each scrolling independently. This snippet builds that with native horizontal scrolling (overflow-x: auto, scroll-behavior: smooth) instead of a JavaScript translateX carousel, then applies the exact same wiring to every .nrc-row found on the page with a single querySelectorAll(...).forEach.

Why native scroll instead of a transform-based track

A transform-based carousel needs its own drag handling, bounds-checking, and snap logic — three carousels' worth of that is three times the code and three times the edge cases. Native scrolling gets free momentum, free bounds (you physically can't scroll past the content), and free touch support from the browser itself. The arrow buttons don't reimplement scrolling; they just call track.scrollBy({ left: ±450, behavior: 'smooth' }), letting the browser's own smooth-scroll do the animating.

Each row is independently self-contained

Because the forEach closure captures its own track, left, and right variables per iteration, each row's scroll position, arrow visibility, and event listeners are completely isolated from every other row — scrolling row two has zero effect on row one's state. That isolation is what makes the pattern trivially stackable: adding a fourth .nrc-row to the HTML wires it up automatically, with no changes to the JS.

Arrows that hide themselves at the edges

A scroll listener on each track checks scrollLeft against 0 and against scrollWidth - clientWidth to toggle each arrow's visibility — so the left arrow disappears at the true start of a row and the right arrow disappears at the true end, rather than staying visible and clickable when there's nothing left to scroll to.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThree labeled rows appear, each auto-populated with eight colored tiles.
  2. 2
    Hover a rowLeft/right arrow buttons fade in over that row only.
  3. 3
    Click an arrowThe row smooth-scrolls by roughly three tiles' width in that direction.
  4. 4
    Scroll to an edgeThe arrow on that side disappears since there's nothing further to scroll to.
  5. 5
    Add a fourth rowCopy an .nrc-row block with a new title — it's wired up automatically by the same forEach loop.

Real-world uses

Common Use Cases

🎬
Streaming platform shelves
Stack Continue Watching, Trending and New Releases rows that each scroll independently, using native horizontal scrolling instead of JavaScript translate maths.
🛒
Recommendation rails
Build You might also like and Frequently bought rows, with one wiring function applied to every .nrc-row however many exist.
📰
Content hub pages
Group articles by topic into rows, with each row's scroll state, tiles and arrows completely isolated from the others.
📊
Widget galleries
Organise saved views or reports into rows, with arrows that fade in on hover and hide automatically at true scroll boundaries.

Got questions?

Frequently Asked Questions

Native scrolling gets free momentum, free boundary clamping, and free touch/trackpad support directly from the browser — a transform-based approach would need to reimplement all three, once per row, for no visual benefit here.

Change the step variable inside the forEach loop (in pixels) — it currently scrolls roughly three tile-widths per click.

Yes — wrap each generated tile's content in an <a> or add a click handler when creating it in the loop; the scroll/arrow logic is unaffected either way.

updateArrows hides an arrow specifically when there is nothing left to scroll toward on that side — check that the row actually has more tiles than fit in the visible width; a row shorter than its container has nothing to scroll.

Arrow buttons are real <button> elements with aria-labels, and because the tracks use native overflow-x: auto, they are also fully operable by keyboard (Tab into the track, then arrow keys or Page Up/Down scroll it) and by screen readers without any extra wiring.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why this carousel uses native overflow-x: auto with scroll-behavior: smooth instead of a JavaScript translateX-based track like most other carousels, and what specific behaviors (momentum, touch, bounds) come free as a result. It's also worth asking the assistant to add lazy-loading for tile images so only the visible ones load initially, or to make each row remember its scroll position across a page reload using localStorage.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a stack of independent, Netflix-style horizontally scrolling content rows in plain HTML, CSS, and vanilla JavaScript, using native browser scrolling rather than a custom transform-based carousel — no library.

Requirements:
- Several row containers stacked vertically, each with its own title heading and its own horizontally-scrollable track using native CSS overflow-x with smooth scroll behavior, not a JavaScript-animated transform.
- A single JavaScript routine that selects every row container present on the page and applies identical setup logic to each one independently — adding a new row to the HTML must automatically wire it up with no JavaScript changes required.
- Each row's track must have its own left and right arrow button that, when clicked, scrolls that specific row's track by a set distance using the native smooth-scroll API, without affecting or referencing any other row's scroll position.
- Arrow buttons must be visually hidden (but present) until the user hovers over that specific row, and must additionally hide themselves automatically whenever that row's track is already scrolled to the corresponding start or end boundary, updating live as the user scrolls.
- The horizontal scrollbar itself must be hidden across browsers while the track remains fully scrollable by mouse drag, trackpad, mouse wheel, and the arrow buttons.
- Each row's tiles/cards must be populated dynamically in JavaScript (not hand-written once per row in the HTML) so the same generation logic produces every row's content.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="nrc-wrap" id="nrcWrap">
  <div class="nrc-row" data-title="Continue Watching">
    <h3 class="nrc-rowTitle">Continue Watching</h3>
    <div class="nrc-rowBody"><button class="nrc-arrow nrc-arrow-l" aria-label="Scroll left">‹</button>
      <div class="nrc-track"></div>
      <button class="nrc-arrow nrc-arrow-r" aria-label="Scroll right">›</button></div>
  </div>
  <div class="nrc-row" data-title="Trending Now">
    <h3 class="nrc-rowTitle">Trending Now</h3>
    <div class="nrc-rowBody"><button class="nrc-arrow nrc-arrow-l" aria-label="Scroll left">‹</button>
      <div class="nrc-track"></div>
      <button class="nrc-arrow nrc-arrow-r" aria-label="Scroll right">›</button></div>
  </div>
  <div class="nrc-row" data-title="Because You Watched Sci-Fi">
    <h3 class="nrc-rowTitle">Because You Watched Sci-Fi</h3>
    <div class="nrc-rowBody"><button class="nrc-arrow nrc-arrow-l" aria-label="Scroll left">‹</button>
      <div class="nrc-track"></div>
      <button class="nrc-arrow nrc-arrow-r" aria-label="Scroll right">›</button></div>
  </div>
</div>