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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | MasonryReviewWallItem[] | — | Testimonials, distributed in order shortest-column-first |
eyebrow | string | "Testimonials" | Small eyebrow above the title (the Chinese version defaults to "客戶評價", "customer reviews") |
title | string | "What customers say" | Section title; also the section's accessible name (Chinese default "客戶怎麼說", "what customers say") |
subtitle | string | — | Supporting text below the title |
columns | 2 | 3 | 4 | 3 | Columns on wide screens; at most 2 below 880px of container width and 1 below 560px |
initialCount | number | 9 | Cards shown while collapsed; the button is hidden when there are no more than this many |
parallax | number | 1 | Scroll parallax multiplier; 0 disables it, 1 shifts each column by 40px |
showRating | boolean | true | Show star ratings when the data has a rating |
expanded | boolean | — | Controlled expanded state; managed internally when omitted |
defaultExpanded | boolean | false | Initial expanded state in uncontrolled mode |
onExpandedChange | (expanded: boolean) => void | — | Callback when the expanded state changes |
moreLabel | string | "Show more testimonials" | Expand button text (Chinese default "顯示更多評價", "show more reviews") |
lessLabel | string | "Collapse" | Collapse button text once everything is shown (Chinese default "收合", "collapse") |
container | RefObject<HTMLElement> | — | Ref of the nested scroll container; omit to use the window as the scroll container |
className | string | — | Added to the root section |
MasonryReviewWallItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Testimonial body text |
name | string | — | Reviewer name; avatar initials use the last two characters of a CJK name or the Latin initials |
role | string | — | Job title |
company | string | — | Company or organisation, joined to the role with "·" |
rating | number | — | Star rating from 1 to 5 |
avatarHue | number | — | Avatar gradient hue; assigned by index via the golden angle when omitted |
highlight | string | — | Fragment of the quote to mark in the accent color |
metric | { value: string; label: string } | — | Result metric shown at the bottom of the card |
featured | boolean | false | Featured 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#0d9488in light mode and#2dd4bfin dark mode) and drives the eyebrow, featured backgrounds, highlighted fragments, metrics and backdrop blobs. - The bottom fade uses CSS
mask-imageto 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 theh2title; without a title it falls back toaria-label - Each testimonial is a
figure/blockquote/figcaption; the star rating is arole="img"with anaria-label, and avatar initials are hidden from assistive technology - The expand button carries
aria-expandedandaria-controls, and anaria-liveregion 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
Sketchnote Pricing
A notebook-paper pricing section for React: hand-drawn borders and check marks stroke in, the recommended plan gets a sketched circle and note, and prices tween between monthly and yearly.
Case Note Pile
Each testimonial becomes a case note — structured rollout details, a paper-clipped photo and a highlighter over the key line — and the selected note is pulled to the front of the pile.