Page Sections

Masonry Review Wall

A masonry wall of testimonial cards of varying heights, each column moving at its own parallax speed as you scroll over a slowly morphing generative backdrop, with a show-more expansion.

A customer testimonial section for landing or pricing pages. Card heights are estimated from quote length and placed shortest-column-first across 2–4 columns, so quotes of different lengths settle into a staggered masonry layout. As you scroll past, columns 1 and 3 drift up while column 2 drifts down, over three large blurred blobs that slowly shift and morph. Only some cards show at first, with a gradient fade and a "show more" button at the bottom; expanding fades the new cards in and the mask out, and the button turns into "collapse". Featured cards (featured) get an accent-tinted background and larger type.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add masonry-review-wall.en

New project or installation trouble? Start here →

Playground

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

1
9
500
Active edited usageDefault props
<MasonryReviewWall/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/masonry-review-wall.en.json

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

Usage

import { MasonryReviewWall } from "@/components/ui/masonry-review-wall";

<MasonryReviewWall
  eyebrow="Testimonials"
  title="What customers say"
  columns={3}
  initialCount={9}
  testimonials={[
    {
      quote: "Three weeks in, prep waste across our stores dropped sharply.",
      name: "Grace Lin",
      role: "Head of Operations",
      company: "Islet Coffee",
      rating: 5,
      highlight: "prep waste across our stores dropped sharply",
      featured: true,
    },
    {
      quote: "No more reworking the rota three times a week.",
      name: "Sophie Wang",
      role: "Dispatch Manager",
      company: "Polaris Freight",
      metric: { value: "−62%", label: "manual rescheduling hours" },
    },
  ]}
/>

Inside a nested scroll container

By default the component tracks window scroll. When it sits inside a fixed-height overflow-y-auto container, pass that container's ref to container. The column parallax follows how far the wall has scrolled inside the container, and the scroll-back on collapse checks against the container's visible area.

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[400px] overflow-y-auto">
  <MasonryReviewWall container={scrollerRef} />
</div>

When the window itself is the scroll container, container can be omitted and behavior is unchanged.

Props

PropTypeDefaultDescription
testimonialsMasonryReviewWallItem[]—Testimonials, distributed in order shortest-column-first
eyebrowstring"Testimonials"Small eyebrow above the title (the Chinese version defaults to "客戶評價", "customer reviews")
titlestring"What customers say"Section title; also the section's accessible name (Chinese default "客戶怎麼說", "what customers say")
subtitlestring—Supporting text below the title
columns2 | 3 | 43Columns on wide screens; at most 2 below 880px of container width and 1 below 560px
initialCountnumber9Cards shown while collapsed; the button is hidden when there are no more than this many
parallaxnumber1Scroll parallax multiplier; 0 disables it, 1 shifts each column by 40px
showRatingbooleantrueShow star ratings when the data has a rating
expandedboolean—Controlled expanded state; managed internally when omitted
defaultExpandedbooleanfalseInitial expanded state in uncontrolled mode
onExpandedChange(expanded: boolean) => void—Callback when the expanded state changes
moreLabelstring"Show more testimonials"Expand button text (Chinese default "顯示更多評價", "show more reviews")
lessLabelstring"Collapse"Collapse button text once everything is shown (Chinese default "收合", "collapse")
containerRefObject<HTMLElement>—Ref of the nested scroll container; omit to use the window as the scroll container
classNamestring—Added to the root section

MasonryReviewWallItem

FieldTypeDefaultDescription
quotestring—Testimonial body text
namestring—Reviewer name; avatar initials use the last two characters of a CJK name or the Latin initials
rolestring—Job title
companystring—Company or organisation, joined to the role with "·"
ratingnumber—Star rating from 1 to 5
avatarHuenumber—Avatar gradient hue; assigned by index via the golden angle when omitted
highlightstring—Fragment of the quote to mark in the accent color
metric{ value: string; label: string }—Result metric shown at the bottom of the card
featuredbooleanfalseFeatured card: 8% accent background and larger type

How it works

  • Column placement is deterministic: card heights are estimated from quote length and each card goes into the currently shortest column, so the server and the browser compute the same result; when expanding, new cards only append to the bottom of columns and cards already shown never switch columns.
  • After mount the column count adapts to the container width (ResizeObserver), so it drops to a single column in a narrow sidebar or on phones.
  • Timing can be overridden with CSS variables: --wb-masonry-review-wall-duration (card entrance, default 500ms, staggered 60ms per card within a column), --wb-masonry-review-wall-backdrop (backdrop morph period, default 24s) and --wb-masonry-review-wall-hover (hover lift and shadow, default 150ms).
  • The accent color comes from --wb-masonry-review-wall-accent (defaults #0d9488 in light mode and #2dd4bf in dark mode) and drives the eyebrow, featured backgrounds, highlighted fragments, metrics and backdrop blobs.
  • The bottom fade uses CSS mask-image to fade the cards themselves rather than laying a solid color on top, so it works on any background; the backdrop animation pauses when the section leaves the viewport.

Accessibility

  • The root is a <section aria-labelledby> pointing at the h2 title; without a title it falls back to aria-label
  • Each testimonial is a figure / blockquote / figcaption; the star rating is a role="img" with an aria-label, and avatar initials are hidden from assistive technology
  • The expand button carries aria-expanded and aria-controls, and an aria-live region announces how many testimonials are shown after expanding or collapsing; when collapsing, the button is scrolled back into view if it left the viewport
  • When the user has reduce motion enabled at the system level, scroll parallax, backdrop morphing and the hover lift are disabled, card entrances become a 150ms fade, and expand / collapse keeps working