Page Sections

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

7000
700
Active edited usageDefault props
<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.json

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

PropTypeDefaultDescription
testimonialsPortraitOverlayQuoteItem[]—List of testimonials
eyebrowstring—Small label above the title
titlestring—Section title; when provided it becomes the section's aria-labelledby
subtitlestring—Supporting text below the title
autoplaybooleantrueWhether to autoplay; always off when the user prefers reduced motion
intervalnumber7000Autoplay interval (ms); reads --wb-portrait-overlay-quote-interval when omitted
durationnumber700Duration (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
indexnumber—Controlled current index; managed internally when omitted
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => void—Called when the index changes
classNamestring—Forwarded to the root section

PortraitOverlayQuoteItem

FieldTypeDefaultDescription
quotestring—Quote text; break lines manually with \n, otherwise lines split automatically at sentence-ending punctuation
namestring—Name, also used for the initials avatar (last two characters for Chinese names, first letters for Latin names)
rolestring—Job title
companystring—Company
ratingnumber—Rating (0–5), shown as stars
avatarHuenumber—Hue of the placeholder portrait and avatar; derived from the index by the golden angle (137.508°) when omitted
highlightstring—Fragment of the quote to underline in the accent colour
metric{ value: string; label: string }—Key figure shown with the quote
photoSrcstring—Portrait photo URL; a gradient plus abstract silhouette stands in when omitted

How it works

  • Timing variables: --wb-portrait-overlay-quote-interval (default 7000ms) and --wb-portrait-overlay-quote-duration (default 700ms). Passing interval / duration writes 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 via aria-labelledby when one is given; the carousel area is role="region" with aria-roledescription="carousel"
  • Each testimonial is role="group" with aria-roledescription="testimonial" and an aria-label such as "2 of 5"; quotes are marked up with figure, blockquote and figcaption
  • aria-live="off" while autoplay runs so screen readers are not interrupted on a timer; it switches to polite when paused or switched manually
  • Previous / next and the pause button are type="button" with an aria-label, and the pause button carries aria-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