Business Card Reel
Business-card-style testimonials laid out on a horizontal reel that you drag or scroll to snap; cards grow sharper and larger towards the centre and shrink, fade and tilt towards the edges.
Customer testimonials as landscape business cards on a full-width horizontal reel. Each card has an initials gradient avatar, the name and role · company at the top left, a company wordmark at the top right, a quote of up to three lines in the middle, and tags such as industry and plan below a hairline divider. The reel uses native scrolling with scroll-snap: touch and trackpads swipe directly, a mouse can press and drag with inertia, and the arrows in the header move one card at a time. Every card scales, fades and tilts with rotateY according to its distance from the reel centre, so the middle card is always the largest and clearest.
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 business-card-reel.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<BusinessCardReel/>
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/business-card-reel.en.jsonOr, once registries are configured in components.json, install it as @webberui/business-card-reel.
Usage
import { BusinessCardReel } from "@/components/ui/business-card-reel";
<BusinessCardReel
eyebrow="Customer stories"
title="The first month after rollout"
testimonials={[
{
quote: "Building the weekly store rota used to take half a day. Now managers finish it in ten minutes.",
name: "Maya Collins",
role: "Operations Manager",
company: "Islet Coffee",
companyMark: "Islet",
highlight: "in ten minutes",
tags: ["Food & drink", "Team plan"],
},
// ...
]}
cardWidth={320}
tilt={8}
onChange={(index) => console.log(`centred card ${index + 1}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | BusinessCardReelItem[] | — | Testimonial items |
eyebrow | string | — | Small label above the title |
title | string | "客戶怎麼說" (What customers say; the English registry defaults to "What customers say") | Section heading, also the aria-labelledby source of the section |
subtitle | string | — | Supporting text below the heading |
value | number | — | Controlled: index of the centred card; changing it scrolls the reel to that card |
defaultValue | number | 0 | Uncontrolled: index of the initially centred card (jumps there after mount, no animation) |
onChange | (index: number) => void | — | Called when scrolling has snapped and the centred card changed |
cardWidth | number | 320 | Card width (px); height follows 1.75:1 and grows when content needs more room |
tilt | number | 8 | Maximum rotateY tilt of the side cards (deg); 0 disables tilting |
showTags | boolean | true | Whether to show the tag chips at the bottom of each card |
duration | number | — | Entrance duration (ms); reads --wb-business-card-reel-duration when omitted (default 500ms) |
reelLabel | string | "客戶評價捲帶,可用左右方向鍵切換" (Customer testimonial reel, use the left and right arrow keys to move) | aria-label of the reel region |
prevLabel | string | "上一則評價" (Previous testimonial) | aria-label of the previous button |
nextLabel | string | "下一則評價" (Next testimonial) | aria-label of the next button |
className | string | — | Merged into the root section |
BusinessCardReelItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote text, clamped to three lines on the card |
name | string | — | Reviewer name; avatar initials use the last two characters for Chinese names and the first letters of the first and last words for Latin names |
role | string | — | Job title |
company | string | — | Company name |
companyMark | string | — | Company wordmark text in the top-right corner; falls back to company |
tags | string[] | — | Tag chips at the bottom of the card (e.g. industry, plan) |
highlight | string | — | Fragment of quote marked in the accent colour (exact match, first occurrence only) |
avatarHue | number | — | Avatar gradient hue (0–360); derived from the index via the 137.508° golden angle when omitted |
avatarSrc | string | — | Avatar image URL; replaces the initials gradient tile |
rating | number | — | Not shown by this component; kept so data can be shared with other testimonial components |
metric | { value: string; label: string } | — | Not shown by this component; kept so data can be shared with other testimonial components |
How it works
- Distance mapping: each card turns the distance between its centre and the reel centre into a ratio d (0–1) and maps it to
scale1 → 0.88,opacity1 → 0.5 androtateY0 → ±tilt, driven by the scroll position fromuseScroll({ container })without re-rendering. - The track is padded by half the reel width minus half a card on each side, so the first and last cards can snap to the exact centre as well.
- Mouse dragging temporarily disables
scroll-snap, applies velocity-based inertia after release, then hands back to snapping on the nearest card; the release does not trigger clicks inside cards. Touch and trackpads keep native scrolling. onChangefires only after scrolling stops and the reel has snapped; cards passed during a smooth scroll are not reported, so controlled mode is never interrupted by intermediate indexes.- The accent goes through
--wb-business-card-reel-accentonly (default#0d9488in light,#2dd4bfin dark) for the eyebrow, quote highlight and focus ring; the entrance duration uses--wb-business-card-reel-duration.
Accessibility
- The root is a
sectionwitharia-labelledbypointing at the heading; each testimonial is afigurewith the quote inblockquoteand the name and role infigcaption - The reel is
role="region"with anaria-labelandtabIndex=0; once focused, ← / → move one card and Home / End jump to either end - The arrow buttons are
type="button"with anaria-labelandaria-controlspointing at the reel, and are disabled at either end - After snapping, an
aria-live="polite"region announces the position ("Testimonial 2 of 6") and the name - When the user has reduce motion enabled, distance scaling, fading and tilting are turned off, arrows jump instantly, dragging has no inertia and the entrance becomes a 150ms fade;
scroll-snapis kept
Portrait Overlay Quote
A React testimonial section with the quote overlaid on a full-bleed portrait; switching crossfades the image with a slow push-in while the text rises line by line from a mask, with optional autoplay and drag to switch.
Instant Photo Stack
A pile of askew instant photos with the quote on the top print; click or flick it away to send it to the bottom, bringing the next one up as the stack re-fans.