Page Sections

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

300
8
6000
Active edited usageDefault props
<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.json

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

PropTypeDefaultDescription
eyebrowstring—Small label above the title
titlestring"客戶怎麼說" (What customers say; the English registry defaults to "What customers say")Section title
subtitlestring—Supporting text below the title
testimonialsInstantPhotoStackItem[]—Testimonials (4–6 recommended)
indexnumber—Controlled mode: index of the testimonial on top
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => void—Called when the index changes
autoplaybooleanfalseAuto-advance (always off under reduced motion)
intervalnumber6000Auto-advance interval (ms)
maxRotatenumber8Maximum scatter angle (deg); every layer's tilt scales with it
nextLabelstring"下一則" (Next; English registry: "Next")Text of the Next button
prevLabelstring"上一則" (Previous; English registry: "Previous")Accessible name of the Previous button
classNamestring—className merged into the root section

InstantPhotoStackItem

FieldTypeDefaultDescription
quotestring—Quote text
namestring—Person's name
rolestring—Job title
companystring—Company name
ratingnumber—Star rating (0–5); shown only when provided
avatarHuenumberby indexHue (0–360) of the placeholder portrait; defaults to a golden-angle hue by index
highlightstring—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
photoSrcstring—Photo URL; without it a gradient + SVG silhouette placeholder is drawn

CSS variables

VariableDefaultDescription
--wb-instant-photo-stack-stiffness300Stiffness 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-stiffness in 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 autoplay on, a pause button appears; hovering the pile, focusing inside it or hiding the tab also pauses it.

Accessibility

  • The root is a section labelled by the title through aria-labelledby
  • The pile is a role="region" with an aria-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 as figure / 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