← TestimonialsOpen in v0
Testimonials 01 — Bento Quotes
Asymmetric bento grid of testimonial cards with a featured two-row quote, five-star rating, CSS-only initials avatars and staggered reveals.
$ npx shadcn@latest add @ludusvibe/testimonials-01Code
Installed to components/sections/testimonials-01.tsx — it's yours to edit.
"use client"
import * as React from "react"
import { motion } from "motion/react"
import { Star } from "lucide-react"
import { fadeUp, stagger, viewport } from "@/lib/motion"
import { cn } from "@/lib/utils"
type Author = {
/** Shown in the CSS-only avatar circle when `src` is omitted. */
initials: string
name: string
role: string
/** Optional real photo URL — replaces the initials circle. */
src?: string
}
type Testimonial = {
quote: string
author: Author
}
export type Testimonials01Props = {
eyebrow?: string
title?: React.ReactNode
description?: string
/** The large card spanning two rows and two columns. */
featured?: Testimonial
testimonials?: Testimonial[]
/** Heading element — use "h1" when this is the page's main heading. */
as?: "h1" | "h2"
className?: string
}
const defaultFeatured: Testimonial = {
quote:
"We replaced three internal dashboards and a mess of spreadsheets with Lumen in a single sprint. Six months later it's the first tab every PM opens in the morning — our weekly business review literally runs on it.",
author: {
initials: "ML",
name: "Maya Lindqvist",
role: "VP of Product, Northwind Labs",
},
}
const defaultTestimonials: Testimonial[] = [
{
quote:
"Setup took one afternoon. By Friday the whole company was looking at the same numbers.",
author: {
initials: "DO",
name: "Daniel Okafor",
role: "Head of Growth, Corely",
},
},
{
quote:
"The fastest analytics tool I've ever used — and I've used all of them.",
author: {
initials: "SM",
name: "Sofia Marchetti",
role: "Data Lead, Pixelhaus",
},
},
{
quote:
"Lumen paid for itself in the first month. We caught a churn signal we'd been missing for a year.",
author: {
initials: "JW",
name: "James Whitmore",
role: "CTO, Driftwave",
},
},
{
quote:
"Our engineers actually like it, which has never once happened with a BI tool.",
author: {
initials: "PR",
name: "Priya Raghavan",
role: "Engineering Manager, Loopdesk",
},
},
{
quote: "I stopped exporting CSVs. That alone was worth the switch.",
author: {
initials: "TH",
name: "Tomás Herrera",
role: "Founder, Brightpath",
},
},
]
function Avatar({ author }: { author: Author }) {
if (author.src) {
return (
<img
src={author.src}
alt={author.name}
className="size-10 shrink-0 rounded-full border object-cover"
/>
)
}
return (
<span
aria-hidden
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary"
>
{author.initials}
</span>
)
}
function AuthorLine({ author }: { author: Author }) {
return (
<figcaption className="flex items-center gap-3">
<Avatar author={author} />
<div>
<div className="text-sm font-medium text-foreground">{author.name}</div>
<div className="text-sm text-muted-foreground">{author.role}</div>
</div>
</figcaption>
)
}
export function Testimonials01({
eyebrow = "Testimonials",
title = "Loved by teams who measure everything",
description = "Don't take our word for it — here's what the people building with Lumen every day have to say.",
featured = defaultFeatured,
testimonials = defaultTestimonials,
as: Heading = "h2",
className,
}: Testimonials01Props) {
return (
<section className={cn("bg-background px-6 py-24", className)}>
<div className="mx-auto max-w-6xl">
<motion.div
initial="hidden"
whileInView="visible"
viewport={viewport}
variants={stagger()}
className="mx-auto max-w-2xl text-center"
>
<motion.p
variants={fadeUp}
className="text-sm font-medium tracking-widest text-primary uppercase"
>
{eyebrow}
</motion.p>
<motion.div variants={fadeUp}>
<Heading className="mt-4 text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
{title}
</Heading>
</motion.div>
<motion.p
variants={fadeUp}
className="mt-5 text-pretty text-muted-foreground md:text-lg"
>
{description}
</motion.p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={viewport}
variants={stagger()}
className="mt-14 grid gap-4 lg:grid-cols-3"
>
{/* Featured card — spans 2 columns and 2 rows on large screens */}
<motion.div variants={fadeUp} className="lg:col-span-2 lg:row-span-2">
<figure className="relative flex h-full flex-col justify-between gap-10 overflow-hidden rounded-2xl border bg-card p-8 md:p-10">
<div
aria-hidden
className="absolute -top-24 -right-24 size-64 rounded-full bg-primary/10 blur-3xl"
/>
<div className="relative">
<span className="flex gap-1 text-primary" aria-hidden>
{Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className="size-4 fill-current" />
))}
</span>
<span className="sr-only">Rated 5 out of 5 stars</span>
<blockquote className="mt-6 text-xl font-medium tracking-tight text-balance md:text-2xl">
“{featured.quote}”
</blockquote>
</div>
<div className="relative">
<AuthorLine author={featured.author} />
</div>
</figure>
</motion.div>
{testimonials.map((testimonial, i) => (
<motion.div key={i} variants={fadeUp}>
<figure className="flex h-full flex-col justify-between gap-8 rounded-2xl border bg-card p-6">
<blockquote className="text-sm text-pretty text-foreground/90">
“{testimonial.quote}”
</blockquote>
<AuthorLine author={testimonial.author} />
</figure>
</motion.div>
))}
</motion.div>
</div>
</section>
)
}