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.
Customer testimonials laid out as a fanned hand of cards. Five to seven portrait cards rotate around one shared pivot below the deck, springing out of a stacked pile with a stagger on entrance. Hover a card with the mouse or focus it with the keyboard and it lifts while the cards on either side lean away. Click it to draw it above the centre of the fan, enlarged for reading, while the rest close up and fade; click again, press Esc or click empty space to put it back, and use the Next button to draw them in order. Behind the deck sits a set of concentric arcs generated from the spread angle. The drawn index 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 fanned-quote-deck.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FannedQuoteDeck/>
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/fanned-quote-deck.en.jsonOr, once registries are configured in components.json, install it as @webberui/fanned-quote-deck.
Usage
import { FannedQuoteDeck } from "@/components/ui/fanned-quote-deck";
<FannedQuoteDeck
title="What customers say"
spread={40}
testimonials={[
{
quote: "Reconciling used to take half a day a week. Now it takes ten minutes.",
name: "Yating Lin",
role: "Operations Manager",
company: "Isle Coffee",
rating: 5,
highlight: "ten minutes",
},
{ quote: "The driver app needed no training.", name: "Boyu Chen", company: "Polaris Logistics" },
// …5–7 items work best
]}
onSelectedChange={(index) => console.log(index)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | FannedQuoteDeckItem[] | — | Testimonials; 5–7 works best for the fan to spread nicely |
eyebrow | string | "Customer stories" | Small label above the title |
title | string | "What customers say" | Section title |
subtitle | string | "Click any card to draw it for reading, or press Next to go through them in order." | Supporting text below the title |
spread | number | 40 | Total fan angle (deg), split evenly across the cards |
showBack | boolean | false | Cards lie face down until drawn, then flip over |
selected | number | null | — | Controlled drawn index (null means none drawn); managed internally when omitted |
defaultSelected | number | null | null | Initial drawn index in uncontrolled mode |
onSelectedChange | (index: number | null) => void | — | Called when the drawn index changes |
nextLabel | string | "Next" | Label of the Next button |
className | string | — | Passed through to the root section |
The Chinese registry version uses Chinese defaults: eyebrow "客戶回饋" (Customer feedback), title "客戶怎麼說" (What customers say), subtitle "點任一張卡片抽出閱讀,或按「下一張」依序翻看。" (Click any card to draw it, or press Next to go through them) and nextLabel "下一張" (Next).
FannedQuoteDeckItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote body (ideally 4–5 lines; longer text is truncated) |
name | string | — | Customer name |
role | string | — | Job title |
company | string | — | Company or brand name |
rating | number | — | 1–5 star rating; stars are hidden when omitted |
avatarHue | number | — | Hue (0–360) for the avatar and card-back gradient; assigned by index via the golden angle when omitted |
highlight | string | — | A fragment of quote to mark in the accent colour (first occurrence) |
metric | { value: string; label: string } | — | Result figure shown at the bottom of the card |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-fanned-quote-deck-stiffness | 340 | Spring stiffness for spreading, lifting, drawing and flipping (damping fixed at 30); read once on mount |
--wb-fanned-quote-deck-accent | #0d9488 (dark #2dd4bf) | Accent for the eyebrow, quote mark, highlight, metric and focus ring |
How it works
- The fan: every card's
transform-originis50% 140%(below the card), and cards only rotate — no translation — so the whole deck spreads around one pivot, with angles split evenly from-spread/2tospread/2. - On hover or focus a card rises 24px and halves its angle, while the cards on each side lean out a further 4°. A drawn card straightens, rises and scales to 1.15×; the rest close to
spread × 0.6at 60% opacity. - Only the entrance spread is staggered (50ms per card); later hover and draw moves respond immediately.
- No photos for avatars: CJK names use their last two characters and Latin names the first and last initials, over a two-tone gradient whose hue steps by the golden angle (137.508°). The result is deterministic, so SSR and client match.
- The backdrop arcs are centred on the fan's pivot, with their span and radial guides derived from
spreadand the card count; coordinates are fixed to two decimals so last-bit trig differences between browsers cannot break hydration. - Touch does not trigger the hover lift (only mouse pointer events count), so a card never gets stuck in its lifted state after a tap.
Accessibility
- The root is a
<section aria-labelledby>pointing at the section title; each testimonial renders asfigure/blockquote/figcaption - Each card is
role="button"witharia-pressedfor its drawn state, anaria-labelwith name, company and "card N of M", andaria-describedbypointing at the quote - The deck uses a roving tabindex: Tab stops on a single card, ← / → (or ↑ / ↓) move focus between cards and lift the focused one, Home / End jump to the ends; Enter or Space draws / returns a card, Esc returns it
- Once drawn, the card's content is written to an
aria-live="polite"region for screen readers - Next is a native
buttonwith the current position shown beside it; every interactive element has a focus-visible ring - When the user's system has reduce motion enabled, the spread, lift and draw moves snap into place and the 3D flip becomes a 150ms crossfade between front and back; content can still be changed via click, keyboard and the Next button
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.
Flip Portrait Tiles
A wall of square portrait tiles that flip over in 3D on hover or tap to reveal the quote on the back, with touch and keyboard support.