Press Quotes
Press quotes section for a film: SVG festival laurels, a huge pull quote with drawn stars and a grid of critic quotes.
- Testimonials
- Film & media
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. Focus ring on the quote cell uses :has() (Chrome 105+, Safari 15.4+, Firefox 121+).
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/press-quotes.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/testimonials/press-quotes.tsx
1/**2 * PressQuotes, an editorial press section for a film: festival laurels3 * drawn in SVG, one huge pull quote with hand-drawn stars, and smaller4 * critics' quotes in a rhythmic newspaper grid. Publications are set as5 * typographic wordmarks. Click any smaller quote to promote it to the lead.6 *7 * Props: quotes (PressQuote[]; the first is the lead), laurels (Laurel[]),8 * title, credit, release, ctaLabel, ctaHref, className.9 * PressQuote: { quote, pub, style, critic, stars? (0..5) }10 * style picks the wordmark voice: "serif-caps" | "condensed" | "italic" |11 * "mono" | "ledger" | "light"12 * Laurel: { award, event }13 *14 * Browser support: all evergreen browsers.15 * Accessibility: quotes are <figure>/<blockquote>/<figcaption>; star ratings16 * are labelled images; smaller quotes are buttons ("Lead with this review")17 * and the lead is announced politely after a swap.18 * Reduced motion: stars and laurels appear without drawing.19 */20import { useState } from "react";21 22export type PubStyle = "serif-caps" | "condensed" | "italic" | "mono" | "ledger" | "light";23export interface PressQuote { quote: string; pub: string; style: PubStyle; critic: string; stars?: number }24export interface Laurel { award: string; event: string }25 26export interface PressQuotesProps {27 quotes?: PressQuote[];28 laurels?: Laurel[];29 title?: string;30 credit?: string;31 release?: string;32 ctaLabel?: string;33 ctaHref?: string;34 className?: string;35}36 37export const DEFAULT_QUOTES: PressQuote[] = [38 { quote: "The quietest film of the year, and the one I still can’t stop thinking about.", pub: "Cahier Noir", style: "italic", critic: "Hélène Marchetti", stars: 5 },39 { quote: "Varga films a salt flat the way other directors film a war. I left the cinema thirsty.", pub: "ScreenWeek", style: "condensed", critic: "Marcus Bell", stars: 5 },40 { quote: "Dunja Petrović barely raises her voice for ninety minutes, then breaks your heart with one look at a kettle.", pub: "The Northern Review", style: "serif-caps", critic: "Owen Achterberg", stars: 5 },41 { quote: "Ninety-eight minutes, eleven lines of dialogue, not one of them wasted.", pub: "Reel/Time", style: "mono", critic: "Sam Okoro", stars: 4 },42 { quote: "Slow, yes. Boring, never. Bring someone you don’t have to talk to afterwards.", pub: "The Morning Ledger", style: "ledger", critic: "Ruth Calloway", stars: 4 },43 { quote: "A small miracle made for €1.4 million and a borrowed truck.", pub: "kino.monat", style: "light", critic: "Jana Weiss", stars: 4 },44];45 46export const DEFAULT_LAURELS: Laurel[] = [47 { award: "Grand Prize", event: "Nordlicht Film Festival 2026" },48 { award: "Official Selection", event: "Festival de Saint-Clair 2026" },49 { award: "Best Actress", event: "Lindau Film Week 2026" },50];51 52function Wordmark({ pub, style }: { pub: string; style: PubStyle }) {53 return <span className="fd-prq-pub" data-s={style}>{pub}</span>;54}55 56const STAR = "M12 2.5l2.7 6.2 6.8.6-5.1 4.5 1.5 6.6L12 16.9l-5.9 3.5 1.5-6.6-5.1-4.5 6.8-.6z";57function Stars({ n, size = 18 }: { n: number; size?: number }) {58 return (59 <span className="fd-prq-stars" role="img" aria-label={`${n} out of 5 stars`}>60 {[0, 1, 2, 3, 4].map((k) => (61 <svg key={k} width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" style={{ ["--k" as string]: k }}>62 <path d={STAR} pathLength={1} className="fd-prq-sline" />63 {k < n && <path d={STAR} className="fd-prq-sfill" />}64 </svg>65 ))}66 </span>67 );68}69 70function LaurelMark({ award, event }: Laurel) {71 const leaves = Array.from({ length: 7 }, (_, i) => i);72 const branch = (flip: boolean) => (73 <g transform={flip ? "translate(200 0) scale(-1 1)" : undefined}>74 <path d="M44 104 C 22 86, 18 52, 34 22" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />75 {leaves.map((i) => {76 const t = i / 6;77 const y = 98 - t * 74;78 const x = 40 - Math.sin(t * Math.PI * 0.9) * 16 + t * 2;79 const a = -30 - t * 26;80 return (81 <g key={i} className="fd-prq-leaf" style={{ ["--i" as string]: i }}>82 <ellipse cx={x - 7} cy={y} rx="9" ry="3.6" transform={`rotate(${a} ${x} ${y})`} fill="currentColor" />83 <ellipse cx={x + 6} cy={y - 5} rx="8" ry="3.2" transform={`rotate(${a + 70} ${x} ${y})`} fill="currentColor" />84 </g>85 );86 })}87 </g>88 );89 return (90 <figure className="fd-prq-laurel">91 <svg viewBox="0 0 200 112" width="240" height="134" aria-hidden="true">{branch(false)}{branch(true)}</svg>92 <figcaption>93 <b>{award}</b>94 <span>{event}</span>95 </figcaption>96 </figure>97 );98}99 100export function PressQuotes({101 quotes = DEFAULT_QUOTES,102 laurels = DEFAULT_LAURELS,103 title = "The Salt Year",104 credit = "A film by Ines Varga",105 release = "In cinemas 14 November",106 ctaLabel = "Find a screening",107 ctaHref = "#screenings",108 className,109}: PressQuotesProps) {110 const [order, setOrder] = useState(() => quotes.map((_, i) => i));111 const [swaps, setSwaps] = useState(0);112 const lead = quotes[order[0]];113 const promote = (pos: number) => {114 setOrder((o) => { const n = [...o]; [n[0], n[pos]] = [n[pos], n[0]]; return n; });115 setSwaps((s) => s + 1);116 };117 return (118 <section className={`fd-prq${className ? " " + className : ""}`} aria-label={`Press for ${title}`}>119 <style>{css}</style>120 <header className="fd-prq-mast">121 <span>{title}</span>122 <span>{credit}</span>123 <span>{release}</span>124 </header>125 126 <div className="fd-prq-laurels">127 {laurels.map((l) => <LaurelMark key={l.event} {...l} />)}128 </div>129 130 <figure className="fd-prq-lead" key={"lead" + order[0]} aria-live={swaps ? "polite" : "off"}>131 {lead.stars != null && <Stars n={lead.stars} size={30} />}132 <blockquote className="fd-prq-lq"><p>{lead.quote}</p></blockquote>133 <figcaption className="fd-prq-lc">134 <Wordmark pub={lead.pub} style={lead.style} />135 <span className="fd-prq-critic">{lead.critic}</span>136 </figcaption>137 </figure>138 139 <div className="fd-prq-grid">140 {order.slice(1).map((qi, k) => {141 const q = quotes[qi];142 return (143 <div key={qi} className="fd-prq-item" data-slot={k}>144 <figure className="fd-prq-fig">145 {q.stars != null && <Stars n={q.stars} size={14} />}146 <blockquote className="fd-prq-q"><p>{q.quote}</p></blockquote>147 <figcaption className="fd-prq-cap">148 <Wordmark pub={q.pub} style={q.style} />149 <span className="fd-prq-critic">{q.critic}</span>150 </figcaption>151 </figure>152 <button type="button" className="fd-prq-hint" onClick={() => promote(k + 1)} aria-label={`Lead with the ${q.pub} review`}>Lead with this</button>153 </div>154 );155 })}156 <a className="fd-prq-cta" href={ctaHref}>157 <span className="fd-prq-ctak">{release}</span>158 <span className="fd-prq-ctal">{ctaLabel}</span>159 <svg width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><path d="M8 20h24M23 11l9 9-9 9" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="square" /></svg>160 </a>161 </div>162 </section>163 );164}165 166const SERIF = `"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif`;167const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;168const MONO = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;169 170const css = `171.fd-prq,.fd-prq *,.fd-prq *::before,.fd-prq *::after{box-sizing:border-box}172.fd-prq{--fd-ease-out:cubic-bezier(.22,1,.36,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;173 --fd-prq-bg:#E9E5DB;--fd-prq-ink:#15130F;--fd-prq-mut:#6B655A;--fd-prq-rule:#15130F;--fd-prq-hair:rgba(21,19,15,.2);--fd-prq-red:#C0291A;174 display:block;max-width:1240px;margin:0 auto;padding:40px 32px 88px;background:var(--fd-prq-bg);color:var(--fd-prq-ink);175 font:400 16px/1.45 ${SERIF};letter-spacing:0;-webkit-font-smoothing:antialiased}176.fd-prq-mast{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;margin:0;padding:10px 0;border-top:3px solid var(--fd-prq-rule);border-bottom:1px solid var(--fd-prq-rule);177 font:500 11px/1.2 ${MONO};letter-spacing:.14em;text-transform:uppercase;color:var(--fd-prq-ink)}178.fd-prq-mast span:nth-child(2){text-align:center}179.fd-prq-mast span:nth-child(3){text-align:right;color:var(--fd-prq-red)}180.fd-prq-laurels{display:flex;justify-content:center;flex-wrap:wrap;gap:24px 56px;margin:0;padding:48px 0 8px}181.fd-prq-laurel{position:relative;width:240px;height:134px;margin:0;padding:0;color:var(--fd-prq-ink)}182.fd-prq-laurel svg{display:block;width:240px;height:134px}183.fd-prq-laurel figcaption{position:absolute;inset:16px 60px 14px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;text-align:center}184.fd-prq-laurel b{display:block;font:700 10.5px/1.15 ${SANS};letter-spacing:.16em;text-transform:uppercase;color:var(--fd-prq-ink)}185.fd-prq-laurel span{display:block;font:italic 400 14px/1.2 ${SERIF};color:var(--fd-prq-ink)}186.fd-prq-leaf{opacity:1;transform-box:fill-box;transform-origin:center;animation:fd-prq-leaf var(--fd-dur-4) var(--fd-spring) both;animation-delay:calc(var(--i) * 70ms + 200ms)}187@keyframes fd-prq-leaf{from{opacity:0;transform:scale(.2)}}188.fd-prq-lead{margin:0;padding:56px 0 64px;border-bottom:1px solid var(--fd-prq-rule);text-align:center}189.fd-prq-lead .fd-prq-stars{justify-content:center;margin:0 0 28px}190.fd-prq-lq{margin:0 auto;padding:0}191.fd-prq-lq p{padding:0;font:400 clamp(2.5rem,6.6vw,6rem)/.98 ${SERIF};letter-spacing:-.035em;color:var(--fd-prq-ink);max-width:15ch;margin:0 auto;text-wrap:balance;animation:fd-prq-in var(--fd-dur-4) var(--fd-ease-out) both}192.fd-prq-lq p::before{content:"\\201C";margin-left:-.42em;color:var(--fd-prq-red)}193.fd-prq-lq p::after{content:"\\201D";color:var(--fd-prq-red)}194@keyframes fd-prq-in{from{opacity:0;transform:translateY(18px)}}195.fd-prq-lc{display:flex;align-items:baseline;justify-content:center;gap:14px;margin:32px 0 0;padding:0}196.fd-prq-lc .fd-prq-pub{font-size:1.5rem}197.fd-prq-critic{font:400 12px/1.2 ${MONO};letter-spacing:.06em;text-transform:uppercase;color:var(--fd-prq-mut)}198.fd-prq-stars{display:flex;gap:4px;color:var(--fd-prq-red)}199.fd-prq-stars svg{display:block;overflow:visible}200.fd-prq-sline{fill:none;stroke:currentColor;stroke-width:1.6;stroke-linejoin:round;stroke-dasharray:1;stroke-dashoffset:0;animation:fd-prq-draw var(--fd-dur-4) var(--fd-ease-out) both;animation-delay:calc(var(--k) * 90ms)}201.fd-prq-sfill{fill:currentColor;animation:fd-prq-fill var(--fd-dur-3) var(--fd-ease-out) both;animation-delay:calc(var(--k) * 90ms + 380ms)}202@keyframes fd-prq-draw{from{stroke-dashoffset:1}}203@keyframes fd-prq-fill{from{opacity:0}}204.fd-prq-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));margin:0;padding:0}205.fd-prq-item{position:relative;display:block;margin:0;padding:32px 28px 40px;border:0;border-right:1px solid var(--fd-prq-hair);border-bottom:1px solid var(--fd-prq-hair);background:transparent;color:inherit;text-align:left;cursor:pointer;206 transition:background-color var(--fd-dur-2) var(--fd-ease-out)}207.fd-prq-item[data-slot="0"]{grid-column:span 5;padding-left:0}208.fd-prq-item[data-slot="1"]{grid-column:span 4}209.fd-prq-item[data-slot="2"]{grid-column:span 3;border-right:0}210.fd-prq-item[data-slot="3"]{grid-column:span 4;padding-left:0}211.fd-prq-item[data-slot="4"]{grid-column:span 4}212.fd-prq-item:hover{background:rgba(21,19,15,.04)}213.fd-prq-item:has(.fd-prq-hint:focus-visible){outline:2px solid rgba(192,41,26,.75);outline-offset:-2px}214.fd-prq-cta:focus-visible{outline:2px solid rgba(192,41,26,.75);outline-offset:-2px}215.fd-prq-fig{margin:0;padding:0}216.fd-prq-fig .fd-prq-stars{margin:0 0 16px}217.fd-prq-q{margin:0;padding:0}218.fd-prq-q p{margin:0;padding:0;font:400 1.25rem/1.3 ${SERIF};letter-spacing:-.01em;color:var(--fd-prq-ink);text-wrap:pretty}219.fd-prq-item[data-slot="0"] .fd-prq-q p{font-size:2rem;line-height:1.12;letter-spacing:-.02em}220.fd-prq-cap{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:20px 0 0;padding:0}221.fd-prq-pub{display:inline-block;color:var(--fd-prq-ink);white-space:nowrap}222.fd-prq-pub[data-s="serif-caps"]{font:600 0.9375rem/1 ${SERIF};letter-spacing:.2em;text-transform:uppercase}223.fd-prq-pub[data-s="condensed"]{font:900 italic 1.25rem/1 ${SANS};letter-spacing:-.06em;text-transform:uppercase}224.fd-prq-pub[data-s="italic"]{font:italic 400 1.375rem/1 ${SERIF};letter-spacing:-.01em}225.fd-prq-pub[data-s="mono"]{font:700 1rem/1 ${MONO};letter-spacing:-.02em;text-transform:uppercase;padding:3px 6px;background:var(--fd-prq-ink);color:var(--fd-prq-bg)}226.fd-prq-pub[data-s="ledger"]{font:700 1.125rem/1 ${SERIF};letter-spacing:-.02em;padding-bottom:3px;border-bottom:3px double var(--fd-prq-ink)}227.fd-prq-pub[data-s="light"]{font:300 1.25rem/1 ${SANS};letter-spacing:-.04em}228.fd-prq-lc .fd-prq-pub[data-s="serif-caps"]{font-size:1.0625rem}229.fd-prq-hint{appearance:none;-webkit-appearance:none;position:absolute;right:16px;bottom:12px;margin:0;padding:0;border:0;border-radius:0;background:transparent;cursor:pointer;outline:none;font:500 10px/1 ${MONO};letter-spacing:.12em;text-transform:uppercase;color:var(--fd-prq-red);opacity:0;transform:translateY(4px);transition:opacity var(--fd-dur-2),transform var(--fd-dur-2) var(--fd-ease-out)}230.fd-prq-hint::after{content:"";position:absolute;inset:-2000px;}231.fd-prq-item{overflow:hidden}232.fd-prq-item:hover .fd-prq-hint,.fd-prq-hint:focus-visible{opacity:1;transform:none}233.fd-prq-cta{grid-column:span 4;display:flex;flex-direction:column;justify-content:space-between;min-height:220px;margin:0;padding:28px;background:var(--fd-prq-red);color:#F6EFE6;text-decoration:none;border-bottom:1px solid var(--fd-prq-red);transition:background-color var(--fd-dur-2)}234.fd-prq-cta:hover{background:#A32014}235.fd-prq-ctak{font:500 11px/1.2 ${MONO};letter-spacing:.14em;text-transform:uppercase}236.fd-prq-ctal{font:400 2.5rem/1 ${SERIF};letter-spacing:-.03em}237.fd-prq-cta svg{display:block;align-self:flex-end;transition:transform var(--fd-dur-3) var(--fd-spring)}238.fd-prq-cta:hover svg{transform:translateX(6px)}239@media (max-width:900px){240 .fd-prq{padding:24px 16px 64px}241 .fd-prq-mast{grid-template-columns:1fr 1fr;font-size:10px}242 .fd-prq-mast span:nth-child(2){display:none}243 .fd-prq-laurels{gap:8px 4px;padding-top:32px}244 .fd-prq-laurel{width:170px;height:96px}245 .fd-prq-laurel svg{width:170px;height:96px}246 .fd-prq-laurel figcaption{inset:14px 38px 10px}247 .fd-prq-laurel span{font-size:11.5px}248 .fd-prq-laurel b{font-size:9px}249 .fd-prq-lead{padding:40px 0 48px}250 .fd-prq-lq p{font-size:2.5rem}251 .fd-prq-lc{flex-direction:column;align-items:center;gap:8px}252 .fd-prq-grid{grid-template-columns:1fr}253 .fd-prq-item,.fd-prq-item[data-slot]{grid-column:auto;padding:28px 0 32px;border-right:0}254 .fd-prq-item[data-slot="0"] .fd-prq-q p{font-size:1.625rem}255 .fd-prq-cta{grid-column:auto;min-height:180px;margin-top:24px}256 .fd-prq-hint{right:0}257}258@media (prefers-reduced-motion:reduce){.fd-prq *{animation:none!important;transition-duration:1ms!important}}259@media (prefers-contrast:more){.fd-prq{--fd-prq-hair:rgba(21,19,15,.6);--fd-prq-mut:#433E35}}260`;261 262export function Demo() {263 return (264 <div className="fd-prqd">265 <style>{`.fd-prqd{min-height:100vh;margin:0;padding:0;background:#E9E5DB}`}</style>266 <PressQuotes />267 </div>268 );269}More Testimonials. Same file contract.
Questions about Press Quotes
Run npx shadcn@latest add https://freedesignmd.com/r/press-quotes.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.
Press quotes section for a film: SVG festival laurels, a huge pull quote with drawn stars and a grid of critic quotes. 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. Focus ring on the quote cell uses :has() (Chrome 105+, Safari 15.4+, Firefox 121+).
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.


