Testimonials
Infinite marquee of customer quotes.
Loved by our teams
“We cut our UI review time in half. Everything just looks consistent now.”
Mara Klein
Design Lead
“The blocks are a cheat code. I shipped a landing page in an afternoon.”
Jonas Weber
Frontend Engineer
“Finally a single source of truth for how our products look and feel.”
Sofia Rossi
Product Manager
“Owning the code instead of a black-box library was the deciding factor.”
Tim Becker
CTO
“Accessibility is handled by default, which our audits confirmed.”
Lea Hoffmann
UX Researcher
“Dark mode, theming, motion: all solved once, used everywhere.”
Noah Fischer
Staff Engineer
Installation
npx shadcn@latest add @l8/testimonialsUsage
import { TestimonialsBlock } from '@/components/blocks/testimonials';
export default function Page() {
return <TestimonialsBlock />;
}Source
"use client"
import { motion } from "motion/react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
const quotes = [
{ name: "Mara Klein", role: "Design Lead", text: "We cut our UI review time in half. Everything just looks consistent now." },
{ name: "Jonas Weber", role: "Frontend Engineer", text: "The blocks are a cheat code. I shipped a landing page in an afternoon." },
{ name: "Sofia Rossi", role: "Product Manager", text: "Finally a single source of truth for how our products look and feel." },
{ name: "Tim Becker", role: "CTO", text: "Owning the code instead of a black-box library was the deciding factor." },
{ name: "Lea Hoffmann", role: "UX Researcher", text: "Accessibility is handled by default, which our audits confirmed." },
{ name: "Noah Fischer", role: "Staff Engineer", text: "Dark mode, theming, motion: all solved once, used everywhere." },
]
export function TestimonialsBlock() {
return (
<section className="py-16">
<h2 className="px-6 text-center text-3xl font-semibold tracking-tight">Loved by our teams</h2>
<div className="mt-10 flex flex-col gap-4 mask-[linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
<Row items={quotes.slice(0, 3)} duration={30} />
<Row items={quotes.slice(3)} duration={36} reverse />
</div>
</section>
)
}
function Row({ items, duration, reverse }: { items: typeof quotes; duration: number; reverse?: boolean }) {
// Content is doubled and shifted by -50% so the loop is seamless.
return (
<div className="flex overflow-hidden">
<motion.div
className="flex shrink-0 gap-4 pr-4"
animate={{ x: reverse ? ["-50%", "0%"] : ["0%", "-50%"] }}
transition={{ duration, repeat: Infinity, ease: "linear" }}
>
{[...items, ...items, ...items, ...items].map((q, i) => (
<figure
key={i}
aria-hidden={i >= items.length}
className="w-72 shrink-0 rounded-xl border bg-card p-5"
>
<blockquote className="text-sm">“{q.text}”</blockquote>
<figcaption className="mt-4 flex items-center gap-3">
<Avatar>
<AvatarFallback>{q.name.split(" ").map((p) => p[0]).join("")}</AvatarFallback>
</Avatar>
<div className="text-sm">
<div className="font-medium">{q.name}</div>
<div className="text-muted-foreground">{q.role}</div>
</div>
</figcaption>
</figure>
))}
</motion.div>
</div>
)
}