webdevpuneet.com

Testimonial Wall — Free HTML CSS JS Marquee Snippet

Testimonial Wall · Cards · Plain HTML, CSS & JS · Live preview

CategoryCards

Copy & paste

Source Code

<section class="tw-section">
  <h2 class="tw-head">Loved by builders</h2>
  <div class="tw-wall" id="twWall" aria-label="Customer testimonials">
    <div class="tw-col" data-speed="38"></div>
    <div class="tw-col" data-speed="50"></div>
    <div class="tw-col tw-hide" data-speed="44"></div>
  </div>
</section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Seamless infinite loop
Duplicated card sets wrap with no visible seam.
Delta-timed animation
rAF with dt keeps speed constant across refresh rates.
Per-column speeds
data-speed drives the layered, organic drift.
Alternating directions
Two-way wrap logic supports up and down scroll.
CSS mask edge fade
Cards fade at the top and bottom with no overlays.
Pause on hover
Stop a column to read, then resume in place.
Data-driven cards
One array with auto-derived avatar initials.
Responsive columns
Drops to two then one column on small screens.

About this UI Snippet

Testimonial Wall — Infinite Scrolling Social-Proof Columns

Screenshot of the Testimonial Wall snippet rendered live

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.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThree columns of review cards scroll vertically and endlessly.
  2. 2
    Watch the layered motionEach column drifts at a different speed and the middle one reverses.
  3. 3
    Note the edge fadesCards melt in and out at the top and bottom via a CSS mask.
  4. 4
    Hover a columnIt pauses so you can read a testimonial, then resumes.
  5. 5
    Swap in real reviewsReplace the DATA array with your own quotes and avatars.
  6. 6
    Tune speeds and fadeEdit data-speed and the mask percentages to taste.

Real-world uses

Common Use Cases

Moving social proof wall
Anchor an endless wall of review cards below a feature tabs showcase, with columns drifting at different speeds.
SaaS homepage credibility
Pair with a rating breakdown summary card so an overall score is followed by individual voices.
Agency client reviews
Follow a logo marquee of client brands with real quotes from the people behind those agency clients.
Static alternative pairing
Complement a testimonial masonry grid with a moving wall, or place a testimonial slider above it for variety.
Infinite loop reference
Study a seamless requestAnimationFrame loop with delta timing, so speed stays constant across screen refresh rates.

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.