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.
Customer testimonials as a pile of askew instant photos on a desk. The left column holds the title, supporting text and a Next button; in the pile on the right, the top print carries the quote and the name, while the prints underneath only show their white borders. Click the top print, press Next or flick it aside, and it springs out, then returns to the bottom of the pile as every other print rises one layer and the stack re-fans. Without a photo, a gradient and an abstract silhouette stand in for the portrait, or pass photoSrc to use a real image. The current index supports controlled and uncontrolled modes, and autoplay is optional.
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 instant-photo-stack.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<InstantPhotoStack/>
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/instant-photo-stack.en.jsonOr, once registries are configured in components.json, install it as @webberui/instant-photo-stack.
Usage
import { InstantPhotoStack } from "@/components/ui/instant-photo-stack";
<InstantPhotoStack
eyebrow="客戶回饋"
title="一張一張翻,都是真心話"
subtitle="點擊相片或按「下一則」換一位客戶。"
testimonials={[
{
quote: "換上新的訂位頁之後,熟客說現在三十秒就能訂好位子。",
name: "林語涵",
role: "店長",
company: "青嶼咖啡",
rating: 5,
highlight: "三十秒就能訂好位子",
},
{
quote: "派車畫面一目了然,新進同仁第一天就能上線。",
name: "陳柏宇",
role: "營運經理",
company: "北辰物流",
metric: { value: "−42%", label: "訓練時數" },
},
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | — | Small label above the title |
title | string | "客戶怎麼說" (What customers say; the English registry defaults to "What customers say") | Section title |
subtitle | string | — | Supporting text below the title |
testimonials | InstantPhotoStackItem[] | — | Testimonials (4–6 recommended) |
index | number | — | Controlled mode: index of the testimonial on top |
defaultIndex | number | 0 | Initial index in uncontrolled mode |
onIndexChange | (index: number) => void | — | Called when the index changes |
autoplay | boolean | false | Auto-advance (always off under reduced motion) |
interval | number | 6000 | Auto-advance interval (ms) |
maxRotate | number | 8 | Maximum scatter angle (deg); every layer's tilt scales with it |
nextLabel | string | "下一則" (Next; English registry: "Next") | Text of the Next button |
prevLabel | string | "上一則" (Previous; English registry: "Previous") | Accessible name of the Previous button |
className | string | — | className merged into the root section |
InstantPhotoStackItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote text |
name | string | — | Person's name |
role | string | — | Job title |
company | string | — | Company name |
rating | number | — | Star rating (0–5); shown only when provided |
avatarHue | number | by index | Hue (0–360) of the placeholder portrait; defaults to a golden-angle hue by index |
highlight | string | — | A fragment of quote to mark in the accent colour |
metric | { value: string; label: string } | — | An optional result figure, shown in the photo's top-left corner |
photoSrc | string | — | Photo URL; without it a gradient + SVG silhouette placeholder is drawn |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-instant-photo-stack-stiffness | 300 | Stiffness of the card-change spring (damping is fixed at 28) |
--wb-instant-photo-stack-accent | #0d9488 (dark #2dd4bf) | Accent colour: eyebrow, highlighted fragment, stars, focus ring |
How it works
- Scatter is decided per layer: each layer has a fixed tilt ratio and an offset within ±12px, scaled by
maxRotate. When a print rises one layer it transitions to that layer's values, so the whole pile re-fans every time; a fixed table instead of randomness keeps SSR and client output identical. - The fling: the top print stays on top and springs out to the upper right (about 1.2× its width, rotating +20°), fading out early in the motion; once it lands it returns to the bottom layer's position and angle, then fades back in.
- While you drag the top print its tilt follows the horizontal offset (±200px maps to ±12°). On release, an offset over 100px or a velocity over 500px/s flings it in that direction and advances; otherwise it springs back.
- The spring stiffness is read from the CSS variable on every change, so overriding
--wb-instant-photo-stack-stiffnessin a theme or a parent container takes effect at any time. - At most 5 layers show at once; extra testimonials wait hidden at the bottom layer until their turn.
- With
autoplayon, a pause button appears; hovering the pile, focusing inside it or hiding the tab also pauses it.
Accessibility
- The root is a
sectionlabelled by the title througharia-labelledby - The pile is a
role="region"with anaria-roledescription, focusable with Tab; Space / Enter / → goes to the next testimonial, ← to the previous one - Only the top print's content is exposed; the others are
aria-hidden. Quotes are marked up asfigure/blockquote/figcaption - On every change an
aria-live="polite"region announces the position and the author's name - Autoplay has a visible pause / resume button (
aria-pressed) and pauses on hover, focus-within and hidden tabs - When the system's reduce motion setting is on, autoplay and dragging are disabled, the fling and re-fan become a 150ms fade out / fade in, and the content still changes via the buttons and keyboard
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.
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.