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.
Turns customer testimonials into a full-bleed portrait poster: a large rounded frame filled with the portrait (a warm two-tone gradient and an abstract bust silhouette stand in when there is no photo), with the white quote and attribution laid over a dark gradient at the bottom. On each switch the old image fades out while the new one fades in and slowly pushes in from 1.08 to 1 across the whole interval; the quote rises line by line out of a mask while the old text drifts up and fades. A thin progress segment per testimonial runs along the top, the bottom right has previous / next, a page count and a pause button, and you can drag the frame left or right to switch.
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 portrait-overlay-quote.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<PortraitOverlayQuote/>
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/portrait-overlay-quote.en.jsonOr, once registries are configured in components.json, install it as @webberui/portrait-overlay-quote.
Usage
import { PortraitOverlayQuote } from "@/components/ui/portrait-overlay-quote";
<PortraitOverlayQuote
eyebrow="What customers say"
title="After they moved their sites to the new interface"
testimonials={[
{
quote: "The redesigned booking page\ncut our weekend waitlist in half.",
name: "Maya Lin",
role: "Head of Operations",
company: "Islet Coffee",
highlight: "in half",
},
{
quote: "Drivers open the dispatch screen and know the next stop. New hires run a route solo on day one.",
name: "Owen Chen",
role: "Dispatch Lead",
company: "Northstar Freight",
photoSrc: "/images/team/chen.jpg",
},
]}
interval={7000}
onIndexChange={(i) => console.log(`Switched to ${i + 1}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | PortraitOverlayQuoteItem[] | — | List of testimonials |
eyebrow | string | — | Small label above the title |
title | string | — | Section title; when provided it becomes the section's aria-labelledby |
subtitle | string | — | Supporting text below the title |
autoplay | boolean | true | Whether to autoplay; always off when the user prefers reduced motion |
interval | number | 7000 | Autoplay interval (ms); reads --wb-portrait-overlay-quote-interval when omitted |
duration | number | 700 | Duration (ms) of the line-by-line text rise; reads --wb-portrait-overlay-quote-duration when omitted |
aspect | "16/9" | "4/3" | "1/1" | "16/9" | Frame aspect ratio from md up; below md it is always 4/5 |
index | number | — | Controlled current index; managed internally when omitted |
defaultIndex | number | 0 | Initial index in uncontrolled mode |
onIndexChange | (index: number) => void | — | Called when the index changes |
className | string | — | Forwarded to the root section |
PortraitOverlayQuoteItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote text; break lines manually with \n, otherwise lines split automatically at sentence-ending punctuation |
name | string | — | Name, also used for the initials avatar (last two characters for Chinese names, first letters for Latin names) |
role | string | — | Job title |
company | string | — | Company |
rating | number | — | Rating (0–5), shown as stars |
avatarHue | number | — | Hue of the placeholder portrait and avatar; derived from the index by the golden angle (137.508°) when omitted |
highlight | string | — | Fragment of the quote to underline in the accent colour |
metric | { value: string; label: string } | — | Key figure shown with the quote |
photoSrc | string | — | Portrait photo URL; a gradient plus abstract silhouette stands in when omitted |
How it works
- Timing variables:
--wb-portrait-overlay-quote-interval(default7000ms) and--wb-portrait-overlay-quote-duration(default700ms). Passinginterval/durationwrites them into these variables, and the component reads them back from the computed style after mounting, so overriding the variables from an outer scope works too. - The accent colour only comes from
--wb-portrait-overlay-quote-accent(light default#0d9488, dark default#2dd4bf), used for the eyebrow, the highlight underline and the progress segments. - A single rAF loop drives both the Ken Burns zoom and the progress segment: while paused the clock stops and the progress holds in place, then picks up where it left off. It pauses on hover, when keyboard focus is inside the section, when the pause button is pressed, when the tab goes to the background, and when the section scrolls off screen.
- The image crossfade is fixed at 800ms — slower than the text, so the picture changes first and the words rise after; each line is staggered by 90ms.
- A drag or horizontal swipe only switches once it moves more than 60px; drags that start on a button are ignored.
Accessibility
- The root element is a
section, labelled by the title viaaria-labelledbywhen one is given; the carousel area isrole="region"witharia-roledescription="carousel" - Each testimonial is
role="group"witharia-roledescription="testimonial"and anaria-labelsuch as "2 of 5"; quotes are marked up withfigure,blockquoteandfigcaption aria-live="off"while autoplay runs so screen readers are not interrupted on a timer; it switches topolitewhen paused or switched manually- Previous / next and the pause button are
type="button"with anaria-label, and the pause button carriesaria-pressed; with focus inside the region the left and right arrow keys switch testimonials - With autoplay on by default there is always a visible pause button (WCAG 2.2.2), and hovering or focusing the region pauses it too
- When the system "Reduce motion" setting is on: autoplay is off, there is no Ken Burns push-in or text movement, images and text fade in over 150ms instead, and the buttons still switch testimonials
Case Note Pile
Each testimonial becomes a case note — structured rollout details, a paper-clipped photo and a highlighter over the key line — and the selected note is pulled to the front of the pile.
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.