A quote card with attribution row of avatar, name, and title on a depth-border surface.
npx shadcn@latest add @paragon/testimonial-cardimport * as React from "react";
import { cn } from "@/lib/utils";
export interface TestimonialCardProps
extends Omit<React.ComponentProps<"figure">, "title"> {
/** The quote body. Curly quotes are added automatically. */
quote?: React.ReactNode;
/** Who said it. Also seeds the initials avatar fallback. */
name?: string;
/** Their role and company, e.g. "VP of Operations, Northwind". */
title?: string;
/** Avatar image URL. Falls back to initials derived from `name`. */
avatarSrc?: string;
}
/** First letters of the first two words — "Amara Chen" → "AC". */
function initialsOf(name: string) {
return name
.trim()
.split(/\s+/)
.slice(0, 2)
.map((word) => word[0])
.join("")
.toUpperCase();
}
/**
* A quote card with an attribution row of avatar, name, and title on a
* depth-border surface. Static by design — testimonials earn attention
* with copy, not motion.
*/
export function TestimonialCard({
quote = "We replaced three internal reporting tools with one dashboard. Finance closes the books two days faster, and nobody has asked me for a CSV export since.",
name = "Amara Chen",
title = "VP of Operations, Northwind",
avatarSrc,
className,
...props
}: TestimonialCardProps) {
return (
<figure
data-slot="testimonial-card"
className={cn(
"flex flex-col justify-between gap-6 rounded-xl bg-card p-6 shadow-border dark:bg-secondary/30",
className,
)}
{...props}
>
<blockquote className="text-pretty text-[15px] leading-relaxed text-foreground">
“{quote}”
</blockquote>
<figcaption className="flex min-w-0 items-center gap-3">
{avatarSrc ? (
<img
src={avatarSrc}
alt=""
className="size-10 shrink-0 rounded-full object-cover shadow-border"
/>
) : (
<span
aria-hidden
className="flex size-10 shrink-0 select-none items-center justify-center rounded-full bg-secondary text-[13px] font-medium text-secondary-foreground shadow-border"
>
{initialsOf(name)}
</span>
)}
<div className="min-w-0">
<div className="truncate text-sm font-medium text-foreground">
{name}
</div>
{title && (
<div className="truncate text-[13px] text-muted-foreground">
{title}
</div>
)}
</div>
</figcaption>
</figure>
);
}