Portrait Quote Carousel
Testimonial carousel: one large serif guest quote at a time over an arched room photo, with a portrait filmstrip.
- Testimonials
- Travel & hospitality
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. Autoplay pauses on hover, focus, off-screen and with reduced motion.
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/portrait-quote-carousel.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/testimonials/portrait-quote-carousel.tsx
1/**2 * PortraitQuoteCarousel, one large serif pull quote at a time, the guest's3 * portrait over a photo of the room they stayed in, stay details set as a4 * small ledger, and a filmstrip of portraits to jump between guests.5 *6 * Props: guests (Guest[]), title, eyebrow, interval (ms, 0 disables autoplay),7 * className.8 * Guest: { name, from, quote, room, nights, when, portrait, photo, photoAlt }9 *10 * Behaviour: the room photo crossfades; the quote reveals line by line (each11 * line rises out of its own mask). Arrows, the filmstrip and the keyboard12 * (Left/Right/Home/End while the carousel has focus) change the guest.13 * Autoplay pauses on hover, on focus and when off-screen, and never runs14 * with reduced motion.15 * Browser support: all evergreen browsers.16 * Accessibility: role="region" with aria-roledescription="carousel"; the17 * filmstrip buttons use aria-current; the quote is announced politely when18 * changed by the user (not during autoplay).19 */20import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from "react";21 22export interface Guest {23 name: string;24 from: string;25 quote: string;26 room: string;27 nights: number;28 when: string;29 portrait: string;30 photo: string;31 photoAlt: string;32}33 34export interface PortraitQuoteCarouselProps {35 guests?: Guest[];36 eyebrow?: string;37 title?: string;38 interval?: number;39 className?: string;40}41 42const H = "/img/halcyon/";43const P = "/img/people/";44 45export const DEFAULT_GUESTS: Guest[] = [46 { name: "Ingrid Solberg", from: "Bergen", portrait: P + "p1.webp", room: "Limonaia Suite", nights: 5, when: "May 2026", photo: H + "suite-900.webp", photoAlt: "The Limonaia Suite, linen bed facing an open window over the bay",47 quote: "I booked two nights and stayed five. On the third morning Rosa left a lemon cake outside the door with a note saying the ferry was cancelled anyway. It wasn’t. I didn’t check." },48 { name: "Omar Farouk", from: "Amman", portrait: P + "p2.webp", room: "Room 7, Sea Terrace", nights: 4, when: "June 2026", photo: H + "pool-lemons-900.webp", photoAlt: "Terrace pool under lemon trees above the sea",49 quote: "We arrived after eleven hours of travel and one lost suitcase. By dinner someone had found me a linen shirt in my size, ironed. I still don’t know whose it was." },50 { name: "Amara Nwosu", from: "London", portrait: P + "p3.webp", room: "Garden Room", nights: 3, when: "September 2026", photo: H + "courtyard-pool-900.webp", photoAlt: "Stone courtyard with a small pool and two loungers",51 quote: "I’m a terrible sleeper and I have complained in hotels all over Italy. Here the only sound at night was the pool filter, and they switched it off at ten without being asked." },52 { name: "Daniel Ortega", from: "Mexico City", portrait: P + "p5.webp", room: "Limonaia Suite", nights: 6, when: "October 2026", photo: H + "positano-sunset-900.webp", photoAlt: "The coast at sunset seen from the villa's upper terrace",53 quote: "Honeymoon. We had a list of nine restaurants. We went to two, then ate on the terrace every night because Paolo’s anchovies were better and nobody had to put shoes on." },54 { name: "Priya Raman", from: "Singapore", portrait: P + "p6.webp", room: "Room 3, Courtyard", nights: 2, when: "April 2026", photo: H + "courtyard-pool-900.webp", photoAlt: "Stone courtyard with lemon trees and a pool",55 quote: "Two nights was a mistake. I have tried to make the ricotta and fig breakfast at home four times now. It is not the same, and Rosa will not tell me why." },56 { name: "Mae Thompson", from: "Atlanta", portrait: P + "p9.webp", room: "Torre Room", nights: 7, when: "March 2026", photo: H + "suite-900.webp", photoAlt: "Bright room with a sea view and a wide bed",57 quote: "My daughter booked it for my seventieth. I was sure the 212 steps down to the water would finish me. By day four I was doing them twice before lunch." },58];59 60function useReduced() {61 const [r, setR] = useState(false);62 useEffect(() => {63 const m = matchMedia("(prefers-reduced-motion: reduce)");64 setR(m.matches);65 const f = () => setR(m.matches);66 m.addEventListener("change", f);67 return () => m.removeEventListener("change", f);68 }, []);69 return r;70}71 72function SplitQuote({ text }: { text: string }) {73 const ref = useRef<HTMLParagraphElement>(null);74 useLayoutEffect(() => {75 const el = ref.current;76 if (!el) return;77 const ws = Array.from(el.querySelectorAll<HTMLElement>("[data-w]"));78 let line = -1;79 let top = -Infinity;80 for (const w of ws) {81 if (w.offsetTop > top + 4) { line++; top = w.offsetTop; }82 w.style.setProperty("--ln", String(line));83 }84 el.setAttribute("data-ready", "");85 }, [text]);86 const words = text.split(" ");87 return (88 <p ref={ref} className="fd-pqc-quote">89 <span className="fd-pqc-sr">{text}</span>90 <span aria-hidden="true">91 {words.map((w, i) => (92 <span key={i} data-w="" className="fd-pqc-w"><span className="fd-pqc-wi">{w}</span>{i < words.length - 1 ? " " : ""}</span>93 ))}94 </span>95 </p>96 );97}98 99export function PortraitQuoteCarousel({100 guests = DEFAULT_GUESTS,101 eyebrow = "From the guest book",102 title = "Fourteen rooms, mostly rebooked.",103 interval = 9000,104 className,105}: PortraitQuoteCarouselProps) {106 const [i, setI] = useState(0);107 const [hold, setHold] = useState(false);108 const [seen, setSeen] = useState(false);109 const [byUser, setByUser] = useState(false);110 const reduced = useReduced();111 const root = useRef<HTMLElement>(null);112 const sx = useRef(0);113 const n = guests.length;114 const g = guests[i];115 const go = useCallback((to: number, user = true) => { setI(((to % n) + n) % n); if (user) setByUser(true); }, [n]);116 117 useEffect(() => {118 const el = root.current;119 if (!el || typeof IntersectionObserver === "undefined") return;120 const io = new IntersectionObserver(([e]) => setSeen(e.isIntersecting), { threshold: 0.35 });121 io.observe(el);122 return () => io.disconnect();123 }, []);124 125 const auto = interval > 0 && !reduced && !hold && seen;126 useEffect(() => {127 if (!auto) return;128 const t = setTimeout(() => go(i + 1, false), interval);129 return () => clearTimeout(t);130 }, [auto, i, interval, go]);131 132 const onKey = (e: KeyboardEvent) => {133 const k = e.key;134 if (k === "ArrowRight") go(i + 1);135 else if (k === "ArrowLeft") go(i - 1);136 else if (k === "Home") go(0);137 else if (k === "End") go(n - 1);138 else return;139 e.preventDefault();140 };141 142 return (143 <section144 ref={root}145 className={`fd-pqc${className ? " " + className : ""}`}146 role="region"147 aria-roledescription="carousel"148 aria-label="Guest stories"149 tabIndex={0}150 onKeyDown={onKey}151 onPointerEnter={() => setHold(true)}152 onPointerLeave={() => setHold(false)}153 onFocus={() => setHold(true)}154 onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setHold(false); }}155 data-auto={auto || undefined}156 >157 <style>{css}</style>158 <div className="fd-pqc-top">159 <p className="fd-pqc-eyebrow">{eyebrow}</p>160 <h2 className="fd-pqc-title">{title}</h2>161 <p className="fd-pqc-count" aria-hidden="true"><b>{String(i + 1).padStart(2, "0")}</b> / {String(n).padStart(2, "0")}</p>162 </div>163 164 <div className="fd-pqc-stage">165 <div166 className="fd-pqc-media"167 onPointerDown={(e) => { sx.current = e.clientX; }}168 onPointerUp={(e) => { const d = e.clientX - sx.current; if (Math.abs(d) > 48) go(i + (d < 0 ? 1 : -1)); }}169 >170 <div className="fd-pqc-arch">171 {guests.map((x, k) => (172 <img key={k} className="fd-pqc-photo" data-on={k === i || undefined} src={x.photo} alt={k === i ? x.photoAlt : ""} aria-hidden={k !== i || undefined} width={900} height={900} loading={k === 0 ? "eager" : "lazy"} decoding="async" />173 ))}174 </div>175 <div className="fd-pqc-portrait" key={"p" + i}>176 <img src={g.portrait} alt={`Portrait of ${g.name}`} width={328} height={328} decoding="async" />177 </div>178 <p className="fd-pqc-caption" aria-hidden="true">{g.room}</p>179 </div>180 181 <div className="fd-pqc-copy" aria-live={byUser ? "polite" : "off"}>182 <svg className="fd-pqc-mark" width="56" height="44" viewBox="0 0 56 44" aria-hidden="true"><path d="M0 44V27C0 11.6 8.2 2.6 22 0l2.4 5.6C16 8.2 12.2 13.4 11.8 20H22v24H0zm32 0V27C32 11.6 40.2 2.6 54 0l2 5.6C47.9 8.2 44.2 13.4 43.8 20H54v24H32z" fill="currentColor" /></svg>183 <SplitQuote key={i} text={g.quote} />184 <div className="fd-pqc-by" key={"b" + i}>185 <p className="fd-pqc-name">{g.name}<span>, {g.from}</span></p>186 <dl className="fd-pqc-ledger">187 <div><dt>Room</dt><dd>{g.room}</dd></div>188 <div><dt>Nights</dt><dd>{g.nights}</dd></div>189 <div><dt>Stayed</dt><dd>{g.when}</dd></div>190 </dl>191 </div>192 </div>193 </div>194 195 <div className="fd-pqc-nav">196 <ol className="fd-pqc-strip" aria-label="Choose a guest">197 {guests.map((x, k) => (198 <li key={k}>199 <button type="button" className="fd-pqc-thumb" aria-current={k === i || undefined} aria-label={`${x.name}, ${x.room}`} onClick={() => go(k)}>200 <img src={x.portrait} alt="" width={64} height={80} loading="lazy" decoding="async" />201 <span className="fd-pqc-bar" aria-hidden="true"><i key={k === i ? "on" + i : "off"} style={{ animationDuration: interval + "ms" }} /></span>202 </button>203 </li>204 ))}205 </ol>206 <div className="fd-pqc-arrows">207 <button type="button" className="fd-pqc-arrow" aria-label="Previous guest" onClick={() => go(i - 1)}>208 <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M16 10H4M9 5l-5 5 5 5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /></svg>209 </button>210 <button type="button" className="fd-pqc-arrow" aria-label="Next guest" onClick={() => go(i + 1)}>211 <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h12M11 5l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /></svg>212 </button>213 </div>214 </div>215 </section>216 );217}218 219const SERIF = `"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif`;220const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;221 222const css = `223.fd-pqc,.fd-pqc *,.fd-pqc *::before,.fd-pqc *::after{box-sizing:border-box}224.fd-pqc{--fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-ease-in-out:cubic-bezier(.65,0,.35,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;--fd-dur-4:700ms;225 --fd-pqc-bg:#EFE9DE;--fd-pqc-ink:#23201A;--fd-pqc-mut:#756C5F;--fd-pqc-line:rgba(35,32,26,.16);--fd-pqc-terra:#A3472A;226 display:block;max-width:1240px;margin:0 auto;padding:72px 32px 64px;background:var(--fd-pqc-bg);color:var(--fd-pqc-ink);outline:none;227 font:400 15px/1.5 ${SANS};letter-spacing:0;-webkit-font-smoothing:antialiased}228.fd-pqc:focus-visible{outline:2px solid rgba(163,71,42,.7);outline-offset:4px}229.fd-pqc-top{display:grid;grid-template-columns:5fr 7fr;align-items:end;column-gap:64px;margin:0 0 48px;padding:0 0 20px;border-bottom:1px solid var(--fd-pqc-line)}230.fd-pqc-eyebrow{grid-column:1;margin:0;padding:0;font:italic 400 1.0625rem/1.3 ${SERIF};color:var(--fd-pqc-terra)}231.fd-pqc-title{grid-column:1;margin:6px 0 0;padding:0;font:400 1.5rem/1.2 ${SERIF};letter-spacing:-.01em;color:var(--fd-pqc-ink)}232.fd-pqc-count{grid-column:2;grid-row:1 / span 2;justify-self:end;margin:0;padding:0;font:400 13px/1 ${SANS};letter-spacing:.14em;color:var(--fd-pqc-mut);font-variant-numeric:tabular-nums}233.fd-pqc-count b{font-weight:600;color:var(--fd-pqc-ink)}234.fd-pqc-stage{display:grid;grid-template-columns:5fr 7fr;column-gap:64px;align-items:center}235.fd-pqc-media{position:relative;margin:0;padding:0 0 28px;touch-action:pan-y;user-select:none;-webkit-user-select:none}236.fd-pqc-arch{position:relative;aspect-ratio:4/5;overflow:hidden;border-radius:9999px 9999px 4px 4px;background:#DCD3C3}237.fd-pqc-photo{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:cover;opacity:0;transform:scale(1.06);transition:opacity var(--fd-dur-4) var(--fd-ease-in-out),transform 1400ms var(--fd-ease-out)}238.fd-pqc-photo[data-on]{opacity:1;transform:scale(1)}239.fd-pqc-portrait{position:absolute;right:-28px;bottom:0;width:38%;max-width:168px;aspect-ratio:4/5;overflow:hidden;margin:0;padding:6px;background:#FBF8F2;box-shadow:0 1px 0 rgba(35,32,26,.08),0 18px 40px -18px rgba(35,32,26,.45);animation:fd-pqc-drop var(--fd-dur-4) var(--fd-ease-out) both}240.fd-pqc-portrait img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 30%}241@keyframes fd-pqc-drop{from{opacity:0;transform:translateY(14px) rotate(2deg)}to{opacity:1;transform:none}}242.fd-pqc-caption{position:absolute;left:0;bottom:0;margin:0;padding:0;font:italic 400 0.8125rem/1 ${SERIF};color:var(--fd-pqc-mut)}243.fd-pqc-copy{position:relative;min-height:440px;display:flex;flex-direction:column;justify-content:center;padding:0}244.fd-pqc-mark{display:block;width:44px;height:auto;margin:0 0 28px;color:var(--fd-pqc-terra)}245.fd-pqc-quote{margin:0;padding:0;font:400 clamp(1.75rem,3.1vw,2.75rem)/1.18 ${SERIF};letter-spacing:-.018em;color:var(--fd-pqc-ink);text-wrap:pretty}246.fd-pqc-w{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.08em;margin-bottom:-.08em;white-space:pre}247.fd-pqc-wi{display:inline-block;transform:translateY(105%)}248.fd-pqc-quote[data-ready] .fd-pqc-wi{animation:fd-pqc-rise var(--fd-dur-4) var(--fd-ease-out) both;animation-delay:calc(var(--ln,0) * 90ms + 60ms)}249@keyframes fd-pqc-rise{from{transform:translateY(105%)}to{transform:translateY(0)}}250.fd-pqc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}251.fd-pqc-by{margin:40px 0 0;padding:0;animation:fd-pqc-fade var(--fd-dur-4) var(--fd-ease-out) 380ms both}252@keyframes fd-pqc-fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}253.fd-pqc-name{margin:0 0 16px;padding:0;font:600 13px/1.3 ${SANS};letter-spacing:.14em;text-transform:uppercase;color:var(--fd-pqc-ink)}254.fd-pqc-name span{font-weight:400;color:var(--fd-pqc-mut)}255.fd-pqc-ledger{display:grid;grid-template-columns:2fr 1fr 1.4fr;margin:0;padding:0;border-top:1px solid var(--fd-pqc-line)}256.fd-pqc-ledger div{margin:0;padding:12px 16px 0 0;border-right:1px solid var(--fd-pqc-line)}257.fd-pqc-ledger div+div{padding-left:16px}258.fd-pqc-ledger div:last-child{border-right:0}259.fd-pqc-ledger dt{margin:0;padding:0;font:500 11px/1.2 ${SANS};letter-spacing:.12em;text-transform:uppercase;color:var(--fd-pqc-mut)}260.fd-pqc-ledger dd{margin:6px 0 0;padding:0;font:400 1.0625rem/1.3 ${SERIF};color:var(--fd-pqc-ink)}261.fd-pqc-nav{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:48px 0 0;padding:24px 0 0;border-top:1px solid var(--fd-pqc-line)}262.fd-pqc-strip{display:flex;gap:10px;margin:0;padding:0;list-style:none;overflow-x:auto;scrollbar-width:none}263.fd-pqc-strip::-webkit-scrollbar{display:none}264.fd-pqc-strip li{margin:0;padding:0;list-style:none;flex:none}265.fd-pqc-thumb{appearance:none;-webkit-appearance:none;position:relative;display:block;width:64px;margin:0;padding:0 0 8px;border:0;background:transparent;cursor:pointer;color:inherit;font:inherit;border-radius:0}266.fd-pqc-thumb img{display:block;width:64px;height:80px;object-fit:cover;object-position:50% 28%;filter:grayscale(1) contrast(.95);opacity:.55;transition:filter var(--fd-dur-3) var(--fd-ease-out),opacity var(--fd-dur-3) var(--fd-ease-out),transform var(--fd-dur-2) var(--fd-spring)}267.fd-pqc-thumb:hover img{opacity:.9;filter:grayscale(.3)}268.fd-pqc-thumb:active img{transform:scale(.96);transition-duration:var(--fd-dur-1)}269.fd-pqc-thumb[aria-current] img{filter:none;opacity:1}270.fd-pqc-thumb:focus-visible,.fd-pqc-arrow:focus-visible{outline:2px solid rgba(163,71,42,.7);outline-offset:2px}271.fd-pqc-bar{position:absolute;left:0;right:0;bottom:0;display:block;height:2px;background:transparent;overflow:hidden}272.fd-pqc-thumb[aria-current] .fd-pqc-bar{background:var(--fd-pqc-line)}273.fd-pqc-bar i{display:block;height:100%;width:100%;background:var(--fd-pqc-terra);transform-origin:0 50%;transform:scaleX(0)}274.fd-pqc-thumb[aria-current] .fd-pqc-bar i{transform:scaleX(1)}275.fd-pqc[data-auto] .fd-pqc-thumb[aria-current] .fd-pqc-bar i{animation:fd-pqc-prog linear both}276@keyframes fd-pqc-prog{from{transform:scaleX(0)}to{transform:scaleX(1)}}277.fd-pqc-arrows{display:flex;gap:8px;flex:none}278.fd-pqc-arrow{appearance:none;-webkit-appearance:none;display:grid;place-items:center;width:52px;height:52px;margin:0;padding:0;border:1px solid var(--fd-pqc-line);border-radius:9999px;background:transparent;color:var(--fd-pqc-ink);cursor:pointer;279 transition:background-color var(--fd-dur-2),color var(--fd-dur-2),border-color var(--fd-dur-2),transform var(--fd-dur-2) var(--fd-spring)}280.fd-pqc-arrow svg{display:block}281.fd-pqc-arrow:hover{background:var(--fd-pqc-ink);border-color:var(--fd-pqc-ink);color:var(--fd-pqc-bg)}282.fd-pqc-arrow:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}283@media (max-width:820px){284 .fd-pqc{padding:48px 16px 40px}285 .fd-pqc-top{grid-template-columns:1fr auto;column-gap:16px;margin-bottom:28px}286 .fd-pqc-title{font-size:1.25rem}287 .fd-pqc-stage{grid-template-columns:1fr;row-gap:32px}288 .fd-pqc-media{width:78%;padding-bottom:24px}289 .fd-pqc-portrait{right:-26%;width:44%}290 .fd-pqc-copy{min-height:0;display:block}291 .fd-pqc-mark{width:32px;margin-bottom:18px}292 .fd-pqc-quote{font-size:1.625rem;line-height:1.22}293 .fd-pqc-by{margin-top:28px}294 .fd-pqc-ledger{grid-template-columns:1.6fr .8fr 1.3fr}295 .fd-pqc-ledger div{padding-right:10px}.fd-pqc-ledger div+div{padding-left:10px}296 .fd-pqc-ledger dd{font-size:0.9375rem}297 .fd-pqc-nav{margin-top:32px;padding-top:20px}298 .fd-pqc-strip{gap:8px}299 .fd-pqc-thumb,.fd-pqc-thumb img{width:44px}.fd-pqc-thumb img{height:56px}300 .fd-pqc-arrows{display:none}301}302@media (prefers-reduced-motion:reduce){303 .fd-pqc *,.fd-pqc *::before{animation:none!important;transition-duration:1ms!important}304 .fd-pqc-wi{transform:none}305 .fd-pqc-photo{transform:none}306}307@media (prefers-contrast:more){.fd-pqc{--fd-pqc-line:rgba(35,32,26,.5);--fd-pqc-mut:#4F473C}}308`;309 310export function Demo() {311 return (312 <div className="fd-pqcd">313 <style>{demoCss}</style>314 <header className="fd-pqcd-bar">315 <span className="fd-pqcd-logo">Villa Saracena<em>Praiano</em></span>316 <nav className="fd-pqcd-nav" aria-label="Hotel">317 <a href="#rooms">Rooms</a><a href="#table">The table</a><a href="#getting-here">Getting here</a>318 </nav>319 <a className="fd-pqcd-book" href="#book">Check dates</a>320 </header>321 <PortraitQuoteCarousel />322 </div>323 );324}325 326const demoCss = `327.fd-pqcd,.fd-pqcd *{box-sizing:border-box}328.fd-pqcd{min-height:100vh;margin:0;padding:0;background:#EFE9DE;color:#23201A;font:400 14px/1.4 ${SANS}}329.fd-pqcd-bar{display:flex;align-items:center;gap:32px;max-width:1240px;height:64px;margin:0 auto;padding:0 32px}330.fd-pqcd-logo{display:flex;align-items:baseline;gap:8px;margin-right:auto;font:400 1.25rem/1 ${SERIF};letter-spacing:.01em;color:#23201A}331.fd-pqcd-logo em{font:italic 400 0.875rem/1 ${SERIF};color:#A3472A}332.fd-pqcd-nav{display:flex;gap:28px}333.fd-pqcd-nav a,.fd-pqcd-book{font:400 13px/1 ${SANS};letter-spacing:.06em;color:#23201A;text-decoration:none}334.fd-pqcd-nav a:hover{text-decoration:underline;text-underline-offset:4px}335.fd-pqcd-book{display:inline-flex;align-items:center;height:36px;padding:0 16px;border:1px solid #23201A;border-radius:9999px}336.fd-pqcd-book:hover{background:#23201A;color:#EFE9DE}337.fd-pqcd a:focus-visible{outline:2px solid rgba(163,71,42,.7);outline-offset:2px}338@media (max-width:820px){.fd-pqcd-bar{padding:0 16px}.fd-pqcd-nav{display:none}}339`;More Testimonials. Same file contract.
Questions about Portrait Quote Carousel
Run npx shadcn@latest add https://freedesignmd.com/r/portrait-quote-carousel.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
Testimonial carousel: one large serif guest quote at a time over an arched room photo, with a portrait filmstrip. It is a free, MIT-licensed React testimonials component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers. Autoplay pauses on hover, focus, off-screen and with reduced motion.
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


