WebberUI

Testimonial Wall

A three-column testimonial wall whose columns scroll in alternating directions, in either an auto infinite-scroll or a scroll-parallax style.

Loading preview…
npx shadcn@latest add https://webberui.com/r/testimonial-wall.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
24
260
16
<TestimonialWall />

Installation

npx shadcn@latest add https://webberui.com/r/testimonial-wall.json

Or, once registries are configured in components.json, install it as @webberui/testimonial-wall.

Usage

import { TestimonialWall, type Testimonial } from "@/components/ui/testimonial-wall";

const testimonials: Testimonial[] = [
  { quote: "Wired it up and it was running in five minutes.", name: "Aria Chen", title: "@aria" },
  { quote: "Ship it. It just works.", name: "Marcus Webb", title: "Loom" },
  // …more testimonials
];

<TestimonialWall items={testimonials} height={480} />

Adjacent columns scroll in opposite directions, and the data is distributed across the columns in round-robin order.

Scroll parallax style

variant="scroll" switches to a parallax driven by scroll position: adjacent columns move in opposite directions as the page scrolls. If the scrolling happens inside a nested overflow container, pass that container's ref to container (it can be omitted for window scrolling).

<TestimonialWall items={testimonials} variant="scroll" height={480} />

Custom cards

<TestimonialWall
  items={testimonials}
  renderItem={(item) => (
    <div className="rounded-lg border p-4">
      <p>{item.quote}</p>
      <span>{item.name}</span>
    </div>
  )}
/>

Props

PropTypeDefaultDescription
itemsTestimonial[]The testimonial data, distributed evenly across the columns in order
columnsnumber3Number of columns; adjacent columns scroll in opposite directions
variant"marquee" | "scroll""marquee"Auto infinite scroll, or a parallax driven by scroll position
speednumber24marquee offset per second (px); only applies to marquee
heightnumber | string480Visible height; anything beyond it is clipped
gapnumber16Spacing between cards (px)
pauseOnHoverbooleantrueFor marquee, whether hovering with the mouse or focusing with the keyboard pauses it
renderItem(item, index) => ReactNodeCustom card rendering
labelstring"使用者評價" (Testimonials)Accessible label for the whole region
containerRefObject<HTMLElement>Ref of the nested scroll container for the scroll variant
classNamestringClass for the outer region
cardClassNamestringClass for the built-in card (has no effect with a custom renderItem)

The Testimonial type: { quote: string; name: string; title?: string; avatar?: string }. When no avatar is provided, the card uses the first character of the name as a placeholder avatar.

How it works

  • marquee advances the offset manually with requestAnimationFrame and loops two copies of the content seamlessly, so the speed is unaffected by how many cards each column holds.
  • scroll tracks the section's progress through the viewport with useScroll and applies the parallax within each column's scrollable overflow range, so no blank edges are ever exposed.
  • A gradient mask fades the top and bottom out, so cards entering and leaving dissolve naturally.

Accessibility

  • When the user has "reduce motion" enabled at the system level, it falls back to a static version that shows every testimonial fully expanded, with no clipping and no animation.
  • The whole thing is a section labelled with aria-label; the built-in cards use semantic figure / blockquote / figcaption.
  • Because marquee duplicates the content to loop seamlessly, the duplicate is marked aria-hidden so assistive technology does not announce it twice.
  • With pauseOnHover enabled, keyboard-focusing a link inside a card also pauses the scrolling, which makes reading and interacting easier.

On this page