Page Sections

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.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.

40
340
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
testimonialsFannedQuoteDeckItem[]—Testimonials; 5–7 works best for the fan to spread nicely
eyebrowstring"Customer stories"Small label above the title
titlestring"What customers say"Section title
subtitlestring"Click any card to draw it for reading, or press Next to go through them in order."Supporting text below the title
spreadnumber40Total fan angle (deg), split evenly across the cards
showBackbooleanfalseCards lie face down until drawn, then flip over
selectednumber | null—Controlled drawn index (null means none drawn); managed internally when omitted
defaultSelectednumber | nullnullInitial drawn index in uncontrolled mode
onSelectedChange(index: number | null) => void—Called when the drawn index changes
nextLabelstring"Next"Label of the Next button
classNamestring—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

FieldTypeDefaultDescription
quotestring—Quote body (ideally 4–5 lines; longer text is truncated)
namestring—Customer name
rolestring—Job title
companystring—Company or brand name
ratingnumber—1–5 star rating; stars are hidden when omitted
avatarHuenumber—Hue (0–360) for the avatar and card-back gradient; assigned by index via the golden angle when omitted
highlightstring—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

VariableDefaultDescription
--wb-fanned-quote-deck-stiffness340Spring 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-origin is 50% 140% (below the card), and cards only rotate — no translation — so the whole deck spreads around one pivot, with angles split evenly from -spread/2 to spread/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.6 at 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 spread and 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 as figure / blockquote / figcaption
  • Each card is role="button" with aria-pressed for its drawn state, an aria-label with name, company and "card N of M", and aria-describedby pointing 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 button with 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