# Quote Card — complete runnable component A full testimonial card you can drop into a Remotion project, plus a zero-dependency HTML/CSS variant for non-Remotion pipelines. Both reveal in reading order, emphasize one phrase, fill stars to the real score, and sign off with the author. ## Timing map (9:16, 30fps, ~10s) | Frame | Beat | |---|---| | 0–8 | Quote mark scales in; card settles | | 8–48 | Quote body, line-by-line (8 + i×6) | | 44–54 | Key-phrase highlight sweep (after its line settles) | | 56–70 | Stars fill left→right to `rating/max` | | 72–88 | Author block slides up, holds | | 88–300 | Everything holds still (screenshot-able) | The card holds motionless for the back half — stillness is what makes a testimonial read as a fact rather than an ad. ## Remotion composition ```tsx import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate, Img, } from "remotion"; export type Review = { quote: string; // verbatim testimonial highlight?: string; // exact substring of `quote` to emphasize rating: number; // e.g. 4.5 max?: number; // default 5 name: string; role?: string; company?: string; avatar?: string; // real photo URL, or omit }; const theme = { bg: "#0E1116", ink: "#F5F7FA", muted: "rgba(245,247,250,.55)", accent: "#FFC24B", font: "Inter, system-ui, sans-serif", quoteFont: "'Newsreader', Georgia, serif", quoteMark: "“", // left double quotation mark }; // Split a quote into clauses for reading-order reveal. // Prefer sentence/clause boundaries; fall back to ~32-char wraps. const toLines = (q: string): string[] => { const parts = q.match(/[^.!?,;]+[.!?,;]?\s*/g) ?? [q]; const lines: string[] = []; let buf = ""; for (const p of parts) { if ((buf + p).length > 34 && buf) { lines.push(buf.trim()); buf = p; } else buf += p; } if (buf.trim()) lines.push(buf.trim()); return lines; }; // Render a line, wrapping the highlight substring in if present. const Line: React.FC<{ text: string; highlight?: string; sweep: number }> = ({ text, highlight, sweep, }) => { if (!highlight || !text.includes(highlight)) return <>{text}; const [before, after] = text.split(highlight); return ( <> {before} {highlight} {after} ); }; export const Testimonial: React.FC = ({ quote, highlight, rating, max = 5, name, role, company, avatar, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const lines = toLines(quote); // Quote mark const markIn = spring({ frame, fps, config: { damping: 14 } }); // Highlight sweep, after the body has revealed const hlStart = 8 + lines.length * 6 + 4; const sweep = interpolate(frame, [hlStart, hlStart + 10], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); // Stars fill, after the quote const fillStart = hlStart + 14; const target = (rating / max) * 100; const starW = interpolate(frame, [fillStart, fillStart + 14], [0, target], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); // Author block slides up last const authStart = fillStart + 16; const auth = spring({ frame: frame - authStart, fps, config: { damping: 200 } }); return (
{theme.quoteMark}
{lines.map((line, i) => { const enter = spring({ frame: frame - (8 + i * 6), fps, config: { damping: 200 } }); return ( ); })}
{/* Stars */}
{"★".repeat(max)}
{"★".repeat(max)}
{/* Author */}
{avatar && ( )}
{name} {[role, company].filter(Boolean).join(", ")}
); }; ``` Register it with a duration long enough for the back-half hold: ```tsx ``` ## Why these choices - **`spring` with `damping: 200`** gives a clean rise with no overshoot — testimonials should feel composed, not bouncy. The quote mark uses lighter damping (14) for a touch of life on the one decorative element. - **Highlight as a gradient ``** wipes a real highlighter behind the phrase without a second element or a mask, and survives line wraps via `box-decoration-break: clone`. - **Stars as two stacked glyph rows clipped by width** render any fractional score (4.5, 4.3) with no half-star asset, and the filled row inherits the brand accent. - **Author after stars** matches reading psychology: claim → proof → who. Reordering weakens trust. ## Zero-dependency HTML/CSS variant For pipelines that capture a headless-browser timeline (or just a web embed). Animations are CSS `@keyframes` with per-line `animation-delay`; if rendering frame-exact video, prefer the Remotion version above so frames stay deterministic. ```html
We shipped in days, not months. Onboarding saved us 10 hours a week.
Dana ReyesHead of Ops, Northwind
``` ```css .card { --bg:#0E1116; --ink:#F5F7FA; background:var(--bg); color:var(--ink); font-family:Inter,system-ui,sans-serif; padding:120px; max-width:1080px; } .qmark { font:200px/.6 'Newsreader',Georgia,serif; color:var(--accent); animation:pop .4s cubic-bezier(.22,1,.36,1) both; } blockquote { font:500 64px/1.25 'Newsreader',Georgia,serif; margin:24px 0 48px; } blockquote span { display:block; opacity:0; animation:rise .5s cubic-bezier(.22,1,.36,1) both; animation-delay:calc(.27s + var(--i) * .2s); } mark { background:linear-gradient(90deg,var(--accent) 100%,transparent 100%); color:inherit; padding:0 .08em; border-radius:3px; -webkit-box-decoration-break:clone; box-decoration-break:clone; animation:sweep .35s ease-out both; animation-delay:1s; } @keyframes sweep { from { background-size:0 100% } to { background-size:100% 100% } } .stars { position:relative; display:inline-block; font-size:52px; letter-spacing:6px; } .track { color:rgba(245,247,250,.20); } .fill { position:absolute; inset:0; overflow:hidden; white-space:nowrap; color:var(--accent); width:0; animation:fill .5s ease-out 1.3s forwards; } @keyframes fill { to { width:calc(var(--rating) / var(--max) * 100%) } } figcaption { display:flex; align-items:center; gap:20px; margin-top:56px; opacity:0; animation:rise .5s ease-out 1.8s both; } .avatar { width:88px; height:88px; border-radius:50%; object-fit:cover; } figcaption b { font-size:38px; } figcaption small { display:block; font-size:30px; color:rgba(245,247,250,.55); } @keyframes pop { from { opacity:0; transform:scale(.7) } to { opacity:1; transform:none } } @keyframes rise { from { opacity:0; transform:translateY(14px) } to { opacity:1; transform:none } } ``` The CSS `mark` sweep uses `background-size` (animatable) rather than a gradient color-stop, since color-stop percentages don't animate in CSS. The star `--rating/--max` math is the same fractional fill as the Remotion version. --- ## Built by the team behind iart.ai This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can turn a list of reviews/quotes into testimonial videos, swap text, and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=ad-video-skills&utm_content=ref_footer&utm_term=testimonial-video)