Page Sections

Dark Portrait Spotlight

A React testimonial section that pairs a large portrait with the quote on a dark layout; a thumbnail row below switches customers, crossfading the portrait from greyscale into colour as the name and role slide in.

Lays customer testimonials out as a dark editorial spread: a large vertical portrait on the left (a warm two-tone gradient and an abstract bust silhouette stand in when there is no photo), and an accent-coloured quote mark, the quote and the attribution on the right — side by side, never overlapping. A row of 56px rounded-square thumbnails below stands for each customer: unselected ones are greyscale and half transparent, while the selected one turns to colour with an accent ring and a small dot, and the ring springs over to its new position. On each switch the new portrait fades in from greyscale to colour while easing back from 1.03 to its normal size, and the old one fades out back into greyscale; the quote rises from below, then the name and role slide in from the left one after another. Switching is manual by default, with optional autoplay.

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 dark-portrait-spotlight.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.

700
Active edited usageDefault props
<DarkPortraitSpotlight/>

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/dark-portrait-spotlight.en.json

Or, once registries are configured in components.json, install it as @webberui/dark-portrait-spotlight.

Usage

import { DarkPortraitSpotlight } from "@/components/ui/dark-portrait-spotlight";

<DarkPortraitSpotlight
  eyebrow="客戶怎麼說"
  title="換上新介面之後"
  testimonials={[
    {
      quote: "改版後的訂位頁,讓週末候位少了一半。",
      name: "林語晴",
      role: "營運主理人",
      company: "青嶼咖啡",
      highlight: "少了一半",
    },
    {
      quote: "司機打開派車畫面就知道下一站,新人第一天就能獨立跑線。",
      name: "陳柏宇",
      role: "調度主管",
      company: "北辰物流",
      photoSrc: "/images/team/chen.jpg",
    },
  ]}
  onValueChange={(i) => console.log(`切到第 ${i + 1} 則`)}
/>

Props

PropTypeDefaultDescription
testimonialsDarkPortraitSpotlightItem[]—Testimonials
eyebrowstring—Small label above the title
titlestring—Section title; used as the section's aria-labelledby when provided
subtitlestring—Supporting text below the title
valuenumber—Controlled current index; managed internally when omitted
defaultValuenumber0Initial index in uncontrolled mode
onValueChange(index: number) => void—Called when the index changes
surface"dark" | "auto""dark"dark is always dark; auto follows the site theme and is white in light mode
autoplaybooleanfalseWhether to autoplay; always off when the user prefers reduced motion
intervalnumber7000Autoplay interval (ms)
durationnumber700Portrait crossfade duration (ms); reads --wb-dark-portrait-spotlight-duration when omitted
grayscalebooleantrueWhether to use the greyscale effect: the portrait goes greyscale to colour and unselected thumbnails are greyscale
classNamestring—Passed through to the root section

DarkPortraitSpotlightItem

FieldTypeDefaultDescription
quotestring—Quote text
namestring—Name, also used for the thumbnail initials (last two characters for CJK names, first letters for Latin names)
rolestring—Job title
companystring—Company
ratingnumber—Rating (0–5); not shown by this component, kept so data can be shared with other testimonial components
avatarHuenumber—Hue for the placeholder portrait and thumbnail; derived from the index by the golden angle (137.508°) when omitted
highlightstring—Fragment of the quote to mark in the accent colour
metric{ value: string; label: string }—Key figure shown with the quote
photoSrcstring—Portrait photo URL; falls back to the gradient and silhouette placeholder, and the thumbnail uses initials

How it works

  • Timing variable: --wb-dark-portrait-spotlight-duration (default 700ms) drives the portrait crossfade and the greyscale-to-colour change. Passing duration writes it into that variable, and the component reads it back from computed style after mounting, so overriding the variable from outside works too. The quote, name and role enter a little faster than the portrait, with the name delayed 150ms and the role a further 80ms.
  • Hovering a thumbnail drops its greyscale from 100% to 50%, timed by --wb-dark-portrait-spotlight-hover-duration (default 150ms).
  • The selected ring shares a layoutId layout animation with a stiffness 400, damping 32 spring, so it slides onto the new thumbnail on each switch.
  • The accent colour comes only from --wb-dark-portrait-spotlight-accent (default #2dd4bf on the dark surface; #0d9488 in light mode with surface="auto"), used for the eyebrow, the large quote mark, the highlighted fragment, the metric and the selected ring.
  • With autoplay on, each testimonial stays for interval milliseconds; hovering, keyboard focus inside the block, the pause button or a background tab all pause it, and resuming restarts a full interval.

Accessibility

  • The root is a section, pointing at the title with aria-labelledby when there is one; the quote is rendered as figure, blockquote and figcaption
  • The thumbnail row is a role="tablist"; each thumbnail is a role="tab" with the name as its aria-label and aria-selected for the current one, using roving tabindex; with focus on a thumbnail, ← → cycle through customers and Home / End jump to the first or last
  • The quote area is a role="tabpanel" labelled by the current thumbnail via aria-labelledby; the portrait is a role="img" labelled "Portrait of …"
  • aria-live="off" while autoplaying so screen readers are not interrupted on a timer, switching to polite on manual switches or when paused; whenever autoplay is on there is a visible pause button (WCAG 2.2.2)
  • Text on the dark surface uses neutral-100 / neutral-400, both above 4.5:1 against the neutral-950 background
  • When the user turns on Reduce Motion: autoplay is off, there is no scaling, movement or greyscale transition, the portrait and text fade in over 150ms, the ring jumps straight to its new position, and thumbnails still switch content