Filterable Review Grid
A dense React grid of small testimonial cards that reflow with a layout animation when filtered by industry, lifting on hover with a border that lights up along the pointer, and expanding in place on click.
A compact grid that fits a lot of customer testimonials into one section. The category chips under the title are collected automatically from each testimonial's category; when you switch, filtered-out cards shrink and fade away while the rest glide into their new positions with a spring layout animation, and the selected-chip pill slides along with them. Each small card shows a star rating, a quote clamped to three lines, an initials gradient avatar and the role and company. On hover or keyboard focus the card lifts and its border picks up an accent glow that follows the pointer; clicking a card expands the full quote in place, clicking again collapses it, and only one card is open at a time. The current category supports controlled and uncontrolled modes.
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 filterable-review-grid.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FilterableReviewGrid/>
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/filterable-review-grid.en.jsonOr, once registries are configured in components.json, install it as @webberui/filterable-review-grid.
Usage
import { FilterableReviewGrid } from "@/components/ui/filterable-review-grid";
<FilterableReviewGrid
eyebrow="客戶怎麼說"
title="各行各業都在用"
columns={4}
testimonials={[
{
quote: "導入後櫃台結帳快了一倍,尖峰時段不再排到門外。",
name: "林語晴",
role: "店長",
company: "青嶼咖啡",
rating: 5,
category: "餐飲",
highlight: "結帳快了一倍",
},
{
quote: "司機端的路線指引很直覺,查件電話少了一大半。",
name: "王子豪",
role: "物流主任",
company: "北辰物流",
rating: 4,
category: "物流",
},
]}
onValueChange={(category) => console.log(category ?? "全部")}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | — | Small label above the title |
title | string | — | Section title |
subtitle | string | — | Supporting text below the title |
testimonials | FilterableReviewGridItem[] | — | Testimonial data |
columns | 2 | 3 | 4 | 4 | Column count on large screens (reduced automatically on smaller screens) |
filterable | boolean | true | Show the category filter chips; when off, every testimonial is shown |
allLabel | string | "All" | Label of the "All" chip |
value | string | null | — | Controlled current category (null means all); managed internally when omitted |
defaultValue | string | null | null | Initial category in uncontrolled mode |
onValueChange | (category: string | null) => void | — | Called when the category changes, with the new category (null means all) |
stiffness | number | — | Spring stiffness of the reflow animation; reads --wb-filterable-review-grid-stiffness (default 350) when omitted |
duration | number | — | Duration of the card hover lift (ms); reads --wb-filterable-review-grid-duration (default 150ms) when omitted |
className | string | — | Class merged into the root section |
FilterableReviewGridItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Full quote text |
name | string | — | Name |
role | string | — | Job title |
company | string | — | Company name |
rating | number | — | Star rating (0–5); the star row is hidden when omitted |
avatarHue | number | — | Avatar gradient hue (0–360); defaults to a golden-angle hue based on the index |
avatarSrc | string | — | Avatar image URL; replaces the initials gradient avatar when provided |
highlight | string | — | A fragment of the quote to mark in the accent color |
metric | { value: string; label: string } | — | An optional result metric |
category | string | — | Industry category; the filter chips are collected automatically from every testimonial's category |
How it works
- The category chips follow the order in which each
categoryfirst appears in the testimonials array; if the current category no longer exists in the data, it falls back to "all". Switching category collapses any expanded card. - The reflow uses
motionlayout animations withAnimatePresence mode="popLayout": filtered-out cards leave the layout immediately and shrink as they fade, while the remaining cards spring into their new positions at the same time. - Border glow: on pointer move the cursor position is written into the card's
--x/--yCSS variables (without a React re-render), and the border layer uses a radial-gradient plus a mask so only the 1px outline lights up. - Avatars need no photos: CJK names use their last two characters and Latin names their initials, on a two-tone gradient whose hue is assigned by index with the golden angle (137.508°), so the output is deterministic and identical on the server and client.
- Timing and color can be overridden with CSS variables:
--wb-filterable-review-grid-stiffness(reflow spring stiffness, default 350),--wb-filterable-review-grid-duration(hover lift duration, default 150ms) and--wb-filterable-review-grid-accent(accent color, default#0d9488,#2dd4bfin dark mode).
Accessibility
- The root element is a
sectionlabelled by its title througharia-labelledby; each testimonial is rendered asfigure/blockquote/figcaption - The category chips are a set of
aria-pressedbuttons inside arole="group"; the number of results is announced througharia-live="polite"(for example "Showing 8 testimonials") - Each card is covered by a
type="button"expand button witharia-expandedandaria-controlspointing at the quote paragraph; when focused with Tab the card lifts the same way and shows a focus-visible ring - The star row is
role="img"with anaria-labelsuch as "4 out of 5 stars"; the decorative dot pattern and gradient avatars are hidden from assistive technology - When the user has "Reduce motion" turned on in their system settings, layout reflow, scale enter/exit and the hover lift are disabled and cards switch with a 150ms fade instead; filtering and expanding still work
Color Block Quotes
A testimonial section React component: large colour blocks carry the quotes; choosing a name from the list recolours and gently squeezes the block, slides the selection bar to the new row and crossfades the quote, over a seeded dot or arc pattern.
Fanned Quote Deck
Testimonial cards fan out like a hand of playing cards; the hovered card lifts as its neighbours make room, and a click draws it to the centre enlarged for reading, with a button to deal the next.