Testimonial Wall — Free HTML CSS JS Marquee Snippet
Testimonial Wall · Cards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Testimonial Wall — Infinite Scrolling Social-Proof Columns

The testimonial wall is the social-proof section every modern landing page reaches for: multiple columns of review cards that scroll vertically and endlessly, each column drifting at a slightly different speed so the motion feels organic rather than mechanical. This snippet builds the whole effect — seamless looping, edge fades, alternating directions, and pause-on-hover — in plain HTML, CSS, and vanilla JavaScript with no animation library.
Seamless infinite loop
The trick to an endless scroll is duplication. Each column renders its set of cards twice (col.innerHTML = html + html), and the animation translates the column upward. The instant the offset reaches exactly one set's height — measured as scrollHeight / 2 — the position wraps by adding that height back, which is visually identical because the second set is a perfect copy of the first. The viewer never sees a seam or a reset jump; the cards appear to flow forever.
A real-time animation loop
Rather than a fixed CSS keyframe, the motion runs on requestAnimationFrame with delta timing. Each frame computes dt, the seconds since the previous frame, and advances the position by speed * dt. Because it is time-based instead of frame-based, the scroll runs at the same real-world velocity whether the display is 60Hz or 120Hz, and a dropped frame doesn't cause a stutter in distance traveled. Each column reads its own data-speed (in pixels per second), so the three columns drift at 38, 50, and 44 px/s for that layered, living feel.
Alternating directions
The middle column scrolls the opposite way from its neighbors via an offset of -1 versus 1. The wrap logic handles both: when scrolling up, the position resets after passing -half; when scrolling down, it resets after crossing 0. This two-way wrap means a single loop body supports both directions without special-casing.
Edge fade with a mask
To avoid hard cut-offs where cards appear and disappear, the wall uses a CSS mask-image: linear-gradient(180deg, transparent, #000 12%, #000 88%, transparent). The mask fades the top and bottom 12% of the container to transparent, so cards melt in and out at the edges instead of popping. This is a pure compositing effect — no extra overlay elements and no impact on layout.
Pause on hover
Each column listens for mouseenter and mouseleave to set a paused flag. While paused, the animation loop keeps running but skips the position update, so a visitor can stop a column and actually read a testimonial — then it resumes exactly where it left off. This is the small touch that turns a decorative marquee into something usable.
Data-driven cards
All testimonials live in one DATA array of quote, name, role, and accent color. A cardHTML function builds each card, deriving the avatar's initials from the name automatically. Each column gets a rotated slice of the array so the three columns don't show the same quotes in the same order. To wire it to real reviews, replace DATA with your API response — the rendering and looping need no changes.
Customizing it
Adjust the data-speed values for faster or calmer motion, change the mask percentages to widen or tighten the fade, add a fourth column on wide screens, or hide the third column on tablets (the snippet already drops to two columns under 760px and one under 480px). Pair it with a rating breakdown summary or a logo marquee for a complete social-proof block.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the seamless-loop trick by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why each column renders its card set twice back to back, why the wrap check compares the scroll position against half of scrollHeight rather than the full height, and how the delta-timed requestAnimationFrame loop keeps the drift speed constant regardless of the display's refresh rate. The same assistant can help optimize it — for instance whether measuring scrollHeight inside the very first animation frame is reliable if images or fonts are still loading and change layout height afterward, or whether nine testimonials duplicated across three columns is enough content to avoid a visibly short, repetitive loop. It's also useful for extending the wall: ask it to fetch testimonials from a real API instead of a static array, add a fourth column on very wide screens, or make the pause-on-hover apply to touch/tap on mobile as well as mouse hover. 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:
Build an infinitely auto-scrolling "testimonial wall" with multiple vertical columns in plain HTML, CSS, and JavaScript using delta-timed requestAnimationFrame — no CSS keyframe animation, no library.
Requirements:
- Multiple columns of review cards, each column receiving a different rotated slice of a shared testimonials data array so no two columns show the same cards in the same order, and each column's full card set rendered twice back-to-back in the DOm so the content can loop seamlessly.
- Each column must read its own scroll speed in pixels per second from a data attribute, and columns must alternate scroll direction (e.g. even-indexed columns scroll one way, odd-indexed the opposite way).
- Drive the vertical motion with requestAnimationFrame computing the elapsed time since the previous frame (delta time) on every tick, and advance each column's position by its speed multiplied by that delta time and its direction — not by a fixed per-frame pixel amount — so the visual speed stays constant regardless of display refresh rate or dropped frames.
- Measure each column's half-height (the height of one un-duplicated card set) once after the duplicated content has rendered, and whenever the running position passes that half-height boundary in either scroll direction, wrap it back by adding or subtracting that half-height so the loop continues with no visible jump or seam, relying on the fact that the duplicated content is pixel-identical to the first set.
- Apply a vertical CSS mask (a linear gradient which is transparent at the very top and bottom and opaque through the middle) over the whole wall container so cards fade in and out softly at the edges instead of being hard-clipped.
- Each column must pause its own position updates on mouseenter and resume on mouseleave, without stopping the underlying animation frame loop itself, so hovering one column to read a card does not affect the others.
- The wall must drop from three columns to two, then to one, at defined responsive breakpoints, hiding one column's cards entirely rather than just visually compressing them.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.
Step by step
How to Use
- 1Paste HTML, CSS, and JSThree columns of review cards scroll vertically and endlessly.
- 2Watch the layered motionEach column drifts at a different speed and the middle one reverses.
- 3Note the edge fadesCards melt in and out at the top and bottom via a CSS mask.
- 4Hover a columnIt pauses so you can read a testimonial, then resumes.
- 5Swap in real reviewsReplace the DATA array with your own quotes and avatars.
- 6Tune speeds and fadeEdit data-speed and the mask percentages to taste.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each column renders its cards twice and scrolls by translateY. When the offset reaches one set's height (scrollHeight / 2), the position wraps by adding that height back. Because the second set is an exact copy of the first, the reset lands on an identical frame, so the loop is seamless.
The rAF loop uses delta timing — it advances by speed times the elapsed seconds each frame — so the velocity is identical on 60Hz and 120Hz displays and survives dropped frames. It also makes pause-on-hover trivial: the loop keeps running but skips the position update while a paused flag is set.
The wall has a CSS mask-image set to a vertical linear-gradient that is transparent at the very top and bottom and opaque in the middle. The mask hides the top and bottom 12% of pixels, so cards fade in and out at the edges with no extra overlay elements and no effect on layout.
Yes. All content lives in the DATA array of quote, name, role, and color. The cardHTML function derives avatar initials from the name automatically. Replace DATA with your reviews API response and the duplication, looping, and masking all keep working unchanged.
Render the columns from your data and keep the rAF loop in a mount effect, storing the animation frame id so you can cancel it on unmount (useEffect cleanup in React, onUnmounted in Vue, ngOnDestroy in Angular). Use refs for the column elements rather than getElementById. The CSS — including the mask-image — ports directly, and in Tailwind the fade can use [mask-image:...] arbitrary values.