l8components

Testimonials

Infinite marquee of customer quotes.

Loved by our teams

“We cut our UI review time in half. Everything just looks consistent now.”
MK
Mara Klein
Design Lead
“The blocks are a cheat code. I shipped a landing page in an afternoon.”
JW
Jonas Weber
Frontend Engineer
“Finally a single source of truth for how our products look and feel.”
SR
Sofia Rossi
Product Manager
“Owning the code instead of a black-box library was the deciding factor.”
TB
Tim Becker
CTO
“Accessibility is handled by default, which our audits confirmed.”
LH
Lea Hoffmann
UX Researcher
“Dark mode, theming, motion: all solved once, used everywhere.”
NF
Noah Fischer
Staff Engineer

Installation

npx shadcn@latest add @l8/testimonials

Usage

import { TestimonialsBlock } from '@/components/blocks/testimonials';

export default function Page() {
  return <TestimonialsBlock />;
}

Source

components/blocks/testimonials.tsx
"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>
  )
}

On this page