Recently Viewed Carousel — Product Strip HTML CSS JS

Recently Viewed Carousel · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Native scroll-snap
Flex track with scroll-snap-type so cards snap into place on swipe, drag, or page — no JS animation loop.
Screenful arrow paging
Arrows scroll by the number of whole cards that fit, revealing a fresh set rather than nudging one card.
Edge-aware arrows
The arrows disable at the start and end, recomputed on every scroll and resize.
Hidden but scrollable bar
The scrollbar is hidden for a clean look while the track stays fully scrollable by touch, trackpad, and arrows.
Save heart with isolated click
A corner heart toggles saved state, with stopPropagation keeping save and open as distinct actions.
Two-line clamped names
Product names clamp to two lines so cards stay a uniform height regardless of title length.
Sale price display
Shows the price with an optional struck-through original for discounted items.
Data-driven and reusable
Renders from a PRODUCTS array — reuse for recently viewed, recommended, or any horizontal product row.

About this UI Snippet

Recently Viewed Carousel — Scroll-Snap Product Strip with Arrow Paging

Screenshot of the Recently Viewed Carousel snippet rendered live

The "Recently viewed" strip — a horizontal row of product cards you can scroll or page through — is on nearly every e-commerce site, because reminding shoppers of items they looked at is one of the highest-converting forms of merchandising. This snippet builds that carousel in plain HTML, CSS, and vanilla JavaScript: smooth scroll-snap, arrow buttons that page by a screenful and disable at the edges, save hearts, and a clean hidden scrollbar.

Native scroll-snap, not a JS animation loop

The track is a flex row with overflow-x: auto and scroll-snap-type: x mandatory, so items snap neatly into place as the user swipes, drags, or pages — using the browser's native, momentum-aware scrolling rather than a JavaScript animation loop. Each card has scroll-snap-align: start, so scrolling always lands on a card edge, never mid-card. This is the right foundation: native scroll is buttery on touch devices, respects the OS's scroll physics, and needs no library, while the arrows are a desktop convenience layered on top.

Arrows that page by a screenful and know the edges

The previous/next arrows scroll by a computed "page" — pageWidth() measures how many whole cards fit in the visible track and scrolls by that many, so a click reveals a fresh set rather than nudging one card. Critically, the arrows disable at the boundaries: updateArrows() checks scrollLeft against 0 and against the maximum scroll, greying out the left arrow at the start and the right arrow at the end. This edge-awareness — recomputed on every scroll and resize — is what makes the paging feel finished; an arrow that does nothing because you're already at the end is a common rough edge this avoids.

Hidden scrollbar, kept scrollable

The scrollbar is hidden (scrollbar-width: none and the WebKit pseudo-element) for a clean look, but the track stays fully scrollable by touch, trackpad, and the arrows — hiding the bar is purely cosmetic and never removes the scrolling itself. This gives the polished, app-like appearance of a custom carousel while keeping all the native scroll behavior.

Product cards with save and click

Each card shows the product image (a gradient placeholder here, ready for a real photo), a two-line clamped name, and a price with an optional struck-through original. A heart button in the corner toggles a saved state, with its click stopped from also triggering the card's own click (which would open the product) — the stopPropagation that keeps "save" and "open" as distinct actions on overlapping targets. The cards lift slightly on hover for tactility.

Data-driven and drop-in

The whole strip renders from a PRODUCTS array, so it's populated from your recently-viewed data (typically read from localStorage or your backend, in view order). Swap the emoji/gradient images for real product photos and wire the card click to your product route and the heart to your wishlist. Because it's compact and self-contained, the same component works for "Recommended," "On sale," or any horizontal product row, not just recently viewed.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to reverse-engineer the paging math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how pageWidth() figures out how many whole cards fit in the visible track and why that number, not a fixed pixel amount, is what the arrows scroll by. The same assistant can help you optimize it — ask whether recomputing pageWidth() on every single arrow click is necessary or whether it could be cached and only recalculated on resize, and whether the scroll and resize listeners calling updateArrows() could be throttled for a smoother experience with many cards. It's also useful for extending the carousel: ask it to persist the saved-heart state to localStorage, add keyboard arrow-key support for paging without a mouse, or lazy-load product images only as cards approach the visible track. Treat the code less like a finished artifact and more like a starting point for a conversation.

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 horizontally scrolling product carousel with arrow paging in plain HTML, CSS, and JavaScript with no library.

Requirements:
- The card track must use native CSS scroll-snap: a flex row with overflow-x auto, scroll-snap-type set to x mandatory, and each card given scroll-snap-align start — the actual scrolling and momentum must come from native browser scrolling, not a JavaScript animation loop.
- Hide the scrollbar visually (using the standard cross-browser scrollbar-hiding CSS properties) while keeping the track fully scrollable by touch, trackpad, and the arrow buttons.
- Implement a function that measures a single card's rendered width plus the gap between cards, divides the track's visible width by that to find how many whole cards currently fit, and returns that count times the card-plus-gap size as the "page" distance.
- Wire the previous and next arrow buttons to scroll the track by that computed page distance (not a fixed pixel value) using smooth scrolling.
- Disable the previous arrow when the track's scroll position is at or near the very start, and disable the next arrow when it is at or near the maximum scrollable position, recalculating this on every scroll event and on window resize.
- Each card must have a heart-shaped save button in its corner that toggles a saved visual state on click without also triggering the card's own "open product" click handler — the two click targets overlap, so the heart's click handling must stop the event from propagating to the card.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA "Recently viewed" strip renders with product cards; the left arrow is disabled at the start.
  2. 2
    Scroll or swipeDrag, swipe, or trackpad-scroll the row — cards snap into place via native scroll-snap.
  3. 3
    Page with arrowsClick the arrows to move by a screenful of cards; they disable at the start and end.
  4. 4
    Save an itemClick a card's heart to toggle saved (it turns red) without opening the product.
  5. 5
    Populate from your dataReplace the PRODUCTS array with your recently-viewed items (from localStorage or your API, in view order).
  6. 6
    Use real images and linksSwap the gradient placeholders for product photos and wire the card click to your product page.

Real-world uses

Common Use Cases

Recently viewed products
Remind shoppers of items they browsed — pair with a fly to cart button on each card.
Recommended and related items
Show "You may also like" or "Frequently bought together" strips on product pages.
On-sale and featured rows
Merchandise discounted or featured products in a scrollable row on the homepage.
Wishlist and saved items
Display saved products with the heart pre-filled, alongside a product quick view.
Category previews
Show a sampling of a category as a horizontal strip linking to the full grid.
Learning scroll-snap carousels
A reference for native scroll-snap and edge-aware paging — compare with a carousel for a full-width slide deck.

Got questions?

Frequently Asked Questions

Track viewed product ids in localStorage (push to an array on each product-page visit, dedupe, cap the length, keep most-recent-first), then on render look up those products and pass them as the PRODUCTS array in that order. For logged-in users, store the history server-side so it follows them across devices. The carousel just renders whatever array you give it.

pageWidth() measures a card's width plus the gap, divides the visible track width by that to find how many whole cards fit, and scrolls by that many cards' worth — so each click advances a full screen of items rather than one card. It recomputes on each click, so it adapts to viewport size and any responsive card-width changes automatically.

Hiding the scrollbar (scrollbar-width: none plus the WebKit pseudo-element) is purely cosmetic for a clean, app-like look — it doesn't disable scrolling. The track remains fully scrollable by touch, trackpad, mouse wheel (with shift), and the arrow buttons. Always keep an alternative way to scroll (the arrows) so mouse-only users without horizontal scroll can still navigate.

The heart and the card share overlapping click areas, so the heart's handler calls e.stopPropagation() to prevent the click from also bubbling to the card's open handler. This keeps the two actions distinct — clicking the heart saves, clicking anywhere else on the card opens it — which is the expected behavior for an action button layered on a clickable card.

In React, render items from the array with .map(), use a ref to the track for scrollBy, and update arrow disabled state from an onScroll handler; in Vue, use v-for with a template ref and @scroll; in Angular, use *ngFor with ViewChild and (scroll). The scroll-snap CSS and paging math port unchanged — only the arrow state and scroll calls use each framework's ref mechanism.