TestimonialsDesign system
Site

Testimonials

Quote cards with highlighted text on the active card.

npx context-ds add testimonials
On the site

Usage

Use it for

Customer quotes on the home page and landing pages, three across on desktop.

Avoid

  • Star ratings
  • Moving or scaling cards on hover
  • Bold quotes
  • Mixed card layouts in one grid

Dependencies: lucide-react

Example

examples/testimonials.tsx
import { Testimonials, type Testimonial } from "@/components/site/testimonials";// The six the home page shows, in its order.const items: Testimonial[] = [  {    id: 23,    quote: "Onboarding couldn't be simpler. Self-serve sign-up gives you an API key immediately, the documentation is thorough, and we were integrating within 10 minutes.",    highlights: ["we were integrating within 10 minutes"],    name: "Nick Khami",    role: "Engineering Manager @ Mintlify",    href: "https://context.dev/blog/mintlify-turns-github-repos-into-branded-docs-sites-with-brand-dev",  },  {    id: 1,    quote: "As soon as you type in the email, we use Context.dev to collect information about your brand and different logos. Context.dev saves us a lot of time right now.",    highlights: ["Context.dev saves us a lot of time right now"],    name: "John Acosta",    role: "Founder @ UsePropane.ai",    href: "https://context.dev/blog/propane-ai-streamlines-onboarding-and-delivers-branded-user-interview-experiences-with-brand-dev",  },  {    id: 16,    quote: "Getting started is very simple. API docs are great and sign-up is self serve, with an API key generated immediately. Took 10 minutes to start integrating.",    highlights: ["Getting started is very simple"],    name: "Luke Ramsden",    role: "CPTO @ Architect (tryarchitect.com)",    href: "https://context.dev/blog/architect-enhances-product-onboarding-and-sales-gtm-with-brand-dev",  },  {    id: 18,    quote: "We're seeing much higher activation rates for our free trials and sign-ups because of it. It's made it a lot simpler for customers to get started with DocsBot and to get to that 'aha' value moment very quickly.",    highlights: ["much higher activation rates", "'aha' value moment"],    name: "Aaron Edwards",    role: "Founder @ DocsBot",    href: "https://context.dev/blog/docsbot-reduces-onboarding-friction-and-boosts-activation-with-brand-dev",  },  {    id: 39,    quote: "The API integration was seamless and expedient. My team was able to get it set up and testing with production workloads within an hour of hearing what Context.dev was!",    highlights: ["testing with production workloads within an hour"],    name: "Christopher Price",    role: "COO @ Sixtyfour",    href: "https://context.dev/customers/sixtyfour-tests-production-scraping-workloads-within-an-hour-with-context-dev",  },  {    id: 21,    quote: "Context.dev offers extensive documentation making the setup super easy. On top of this, there is a dedicated Slack channel in case you have follow-up questions. Overall the API integration process was very smooth.",    highlights: ["extensive documentation", "dedicated Slack channel"],    name: "Vlad Veselukha",    role: "Senior Data Engineer @ Vizzy",    href: "https://context.dev/blog/vizzy-beautifies-profiles-and-job-boards-with-brand-dev",  },];export function Site() {  return (    <div className="flex w-full flex-col gap-10">      <div className="text-center">        <h2 className="text-h2 leading-tight tracking-tight text-fg">          <span className="text-fg-brand">Success</span> stories        </h2>        <p className="mx-auto mt-4 max-w-md text-body leading-relaxed text-fg-muted">Feedback from the engineers shipping with us every day.</p>      </div>      <Testimonials items={items} />    </div>  );}

Source

components/site/testimonials.tsx
"use client";import * as React from "react";import { ArrowUpRight } from "lucide-react";import { cn } from "@/lib/utils";// The home page's "Success stories": quote cards on a hairline, every card the same anatomy (quote on top, who said it// along the bottom) and every row as tall as its tallest card, so the set reads as one. Hovering or focusing a card only// brings its color in (the highlighted phrases turn brand blue, the photo leaves grayscale); nothing moves or resizes.type Testimonial = {  id?: string | number;  quote: string;  /** Phrases inside the quote to emphasize; they turn brand blue on the active card. */  highlights?: string[];  name: string;  /** The line under the name, e.g. "Engineering Manager @ Mintlify". */  role: string;  /** Photo URL, shown in grayscale until the card is active. */  avatar?: string;  /** The case study; the whole card becomes the link. */  href?: string;  linkLabel?: string;};function escapeRegExp(text: string) {  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");}function HighlightedQuote({ text, highlights }: { text: string; highlights?: string[] }) {  if (!highlights?.length) return <>{text}</>;  const set = new Set(highlights);  const parts = text.split(new RegExp(`(${highlights.map(escapeRegExp).join("|")})`, "g"));  return <>{parts.map((part, i) => (set.has(part) ? <strong key={i}>{part}</strong> : part))}</>;}function TestimonialCard({ testimonial, active = false, className, ...props }: React.ComponentProps<"figure"> & { testimonial: Testimonial; active?: boolean }) {  const { quote, highlights, name, role, avatar, href, linkLabel = "Read the case study" } = testimonial;  return (    <figure      data-slot="testimonial-card"      data-active={active || undefined}      className={cn("group relative flex flex-col rounded-card p-6 shadow-card transition-[background-color] duration-300 ease-out hover:bg-subtle/60 motion-reduce:transition-none", className)}      {...props}    >      {href && (        <a href={href} aria-label={`${linkLabel}: ${name}`} title={linkLabel} className="absolute inset-0 z-10 rounded-card outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2">          <span className="absolute top-4 right-4 flex size-8 items-center justify-center rounded-pill text-fg-faint transition-[background-color,color] duration-200 ease-out group-hover:bg-tint group-hover:text-fg-brand motion-reduce:transition-none">            <ArrowUpRight aria-hidden="true" className="size-4" />          </span>        </a>      )}      <blockquote        className={cn(          "pr-8 text-body leading-relaxed text-fg-nav [&_strong]:font-medium [&_strong]:transition-[color] [&_strong]:duration-300 [&_strong]:ease-out motion-reduce:[&_strong]:transition-none",          active ? "[&_strong]:text-fg-brand" : "[&_strong]:text-fg",        )}      >        &ldquo;        <HighlightedQuote text={quote} highlights={highlights} />        &rdquo;      </blockquote>      <figcaption className="mt-auto flex min-w-0 items-center gap-3 pt-6">        {avatar && (          <span className="size-10 shrink-0 overflow-hidden rounded-card bg-subtle">            <img src={avatar} alt={name} width={40} height={40} className={cn("size-full object-cover transition-[filter] duration-300 ease-out motion-reduce:transition-none", !active && "grayscale")} />          </span>        )}        <span className="min-w-0">          <span className="block truncate text-body-sm leading-5 font-medium text-fg">{name}</span>          <span className="block truncate text-body-sm text-fg-subtle" title={role}>            {role}          </span>        </span>      </figcaption>    </figure>  );}/** The grid: one column, two from md, three from lg. The first card is active until another is hovered or focused. */function Testimonials({ items, className, ...props }: React.ComponentProps<"div"> & { items: Testimonial[] }) {  const key = (t: Testimonial, i: number) => String(t.id ?? i);  const first = items[0] ? key(items[0], 0) : "";  const [active, setActive] = React.useState(first);  return (    <div      data-slot="testimonials"      className={cn("grid auto-rows-fr gap-4 md:grid-cols-2 lg:grid-cols-3", className)}      onMouseLeave={() => setActive(first)}      onBlur={(event) => {        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setActive(first);      }}      {...props}    >      {items.map((t, i) => (        <TestimonialCard key={key(t, i)} testimonial={t} active={active === key(t, i)} onMouseEnter={() => setActive(key(t, i))} onFocus={() => setActive(key(t, i))} />      ))}    </div>  );}export { Testimonials, TestimonialCard, type Testimonial };

Reference: src/components/sections/home/testimonials.tsx.