Skip to content
Marketing block

Scroll-Snap Quote Rail

Twenty quotes in the height of six, scrolled by CSS snap points with arrows that disable at the ends — no auto-advance, and every quote stays in the DOM for search and Cmd-F.

270 lineslucide-reactAdded 24 Aug 2026Updated 13 Sept 2026
  • testimonials
  • carousel
  • scroll snap
  • quotes
  • rail

What's included

  • components/testimonial-carousel.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add testimonial-carousel

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Customers

Twenty months of people telling us what they actually thought

  • 5 out of 5

    “The migration took an afternoon, which I did not believe when sales said it. What actually saved us was that our old exports imported without a schema rewrite.”

    Dara OkonkwoStaff Engineer, Northwind Freight
  • 5 out of 5

    “We bought it for one team and it spread to four without anyone asking me to approve a rollout. That has happened twice in nine years here.”

    Priya RamachandranDirector of Platform, Alcove Health
  • 5 out of 5

    “Support answered a schema question at 2am our time with an actual query, not a docs link. I have kept the thread.”

    Tomas LindqvistData Lead, Ravel Studio
  • 4 out of 5

    “It is not the cheapest thing we evaluated. It is the only one where the trial ended and nobody asked to go back.”

    Amara BelhadjVP Engineering, Kestrel Systems
  • 5 out of 5

    “Our audit went from a three-week scramble to a report we generate. I know that sounds like copy off your own website. It is what happened.”

    Ruth MbekiHead of Compliance, Fieldstone Bank
  • 4 out of 5

    “The onboarding was rough and we nearly gave up in week one. Two releases later the thing we were stuck on is a checkbox.”

    Jonas WeberPrincipal Architect, Meridian Logistics

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/testimonial-carousel.tsx
'use client'

/**
 * <TestimonialCarousel> — a scroll-snap rail for more quotes than a wall can hold.
 *
 * <TestimonialGrid> is the right shape for six quotes and the wrong one for
 * twenty: a wall that tall stops being social proof and becomes a section
 * the reader scrolls past to reach the pricing. This block fixes the height
 * and moves the overflow sideways.
 *
 * It is deliberately not the carousel that word usually implies. There is no
 * auto-advance — a section that moves on its own takes the reading pace away
 * from the reader, and it is the most complained-about pattern on marketing
 * pages. There is no slide-swapping either: every quote stays in the DOM,
 * which is what keeps the section findable with Cmd-F, quotable by a search
 * engine, and printable.
 *
 * The scrolling itself is CSS. `snap-x snap-mandatory` on the rail and
 * `snap-start` on the cards give trackpad, touch and shift-wheel the right
 * behaviour with no JavaScript at all, so the section still works if the
 * bundle never arrives. The arrows are the enhancement on top, and they are
 * the only reason this file needs a client boundary.
 *
 * Two details a rail like this usually gets wrong:
 *
 *   - The rail is `tabIndex={0}` with a role and a label. A scrollable
 *     region that cannot take focus is content a keyboard user cannot reach
 *     — WCAG 2.1.1 — because the arrow keys have to be sent somewhere.
 *   - The arrows disable at the ends rather than wrapping. A control that
 *     silently loops gives the reader no way to tell they have seen
 *     everything, which for a finite list of quotes is the one thing they
 *     want to know.
 */

import * as React from 'react'
import { ChevronLeft, ChevronRight, Star } from 'lucide-react'

export interface CarouselTestimonial {
  quote: string
  name: string
  role: string
  company: string
  rating?: number
}

export interface TestimonialCarouselProps {
  eyebrow?: string
  heading?: string
  testimonials?: CarouselTestimonial[]
  className?: string
}

const DEFAULT_TESTIMONIALS: CarouselTestimonial[] = [
  {
    quote:
      'The migration took an afternoon, which I did not believe when sales said it. What actually saved us was that our old exports imported without a schema rewrite.',
    name: 'Dara Okonkwo',
    role: 'Staff Engineer',
    company: 'Northwind Freight',
    rating: 5,
  },
  {
    quote:
      'We bought it for one team and it spread to four without anyone asking me to approve a rollout. That has happened twice in nine years here.',
    name: 'Priya Ramachandran',
    role: 'Director of Platform',
    company: 'Alcove Health',
    rating: 5,
  },
  {
    quote:
      'Support answered a schema question at 2am our time with an actual query, not a docs link. I have kept the thread.',
    name: 'Tomas Lindqvist',
    role: 'Data Lead',
    company: 'Ravel Studio',
    rating: 5,
  },
  {
    quote:
      'It is not the cheapest thing we evaluated. It is the only one where the trial ended and nobody asked to go back.',
    name: 'Amara Belhadj',
    role: 'VP Engineering',
    company: 'Kestrel Systems',
    rating: 4,
  },
  {
    quote:
      'Our audit went from a three-week scramble to a report we generate. I know that sounds like copy off your own website. It is what happened.',
    name: 'Ruth Mbeki',
    role: 'Head of Compliance',
    company: 'Fieldstone Bank',
    rating: 5,
  },
  {
    quote:
      'The onboarding was rough and we nearly gave up in week one. Two releases later the thing we were stuck on is a checkbox.',
    name: 'Jonas Weber',
    role: 'Principal Architect',
    company: 'Meridian Logistics',
    rating: 4,
  },
]

export function TestimonialCarousel({
  eyebrow = 'Customers',
  heading = 'Twenty months of people telling us what they actually thought',
  testimonials = DEFAULT_TESTIMONIALS,
  className = '',
}: TestimonialCarouselProps) {
  /*
    Ids come from `useId`, not from a literal.

    A literal is correct until the section appears twice on one page — and
    then `aria-labelledby` resolves to whichever copy is first in the
    document, so the second one is announced with the first one's heading.
    It renders identically and reads wrongly, which is why nothing catches
    it but an id check.
  */
  const headingId = React.useId()

  const railRef = React.useRef<HTMLUListElement>(null)
  const [atStart, setAtStart] = React.useState(true)
  const [atEnd, setAtEnd] = React.useState(false)

  /*
    One handler for both ends, run on scroll and once on mount. The 1px slack
    absorbs sub-pixel scroll positions, which otherwise leave the "next"
    arrow enabled at the very end on fractional-DPI displays.

    `Math.abs` because right-to-left, `scrollLeft` starts at 0 and runs
    NEGATIVE towards the end. Read raw, the rail reports itself at the
    start forever and never reaches the end.
  */
  const syncEnds = React.useCallback(() => {
    const rail = railRef.current
    if (!rail) return
    const offset = Math.abs(rail.scrollLeft)
    setAtStart(offset <= 1)
    setAtEnd(offset + rail.clientWidth >= rail.scrollWidth - 1)
  }, [])

  React.useEffect(() => {
    syncEnds()
    const rail = railRef.current
    if (!rail) return
    const observer = new ResizeObserver(syncEnds)
    observer.observe(rail)
    return () => observer.disconnect()
  }, [syncEnds])

  /** Scroll by one card, measured off the rail rather than a magic number. */
  const nudge = (direction: 1 | -1) => {
    const rail = railRef.current
    if (!rail) return
    const card = rail.firstElementChild as HTMLElement | null
    const step = card ? card.offsetWidth + 24 : rail.clientWidth * 0.8
    // `scrollBy` takes a physical offset, and "next" is leftwards in RTL.
    const physical = getComputedStyle(rail).direction === 'rtl' ? -1 : 1
    rail.scrollBy({ left: step * direction * physical, behavior: 'smooth' })
  }

  const arrowClass =
    'flex h-10 w-10 items-center justify-center rounded-full border border-border/60 text-foreground transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background'

  return (
    <section
      aria-labelledby={headingId}
      className={`mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
    >
      <div className="flex flex-wrap items-end justify-between gap-6">
        <div className="max-w-2xl">
          <p className="text-sm font-medium uppercase tracking-wider text-primary">{eyebrow}</p>
          <h2
            id={headingId}
            className="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl"
          >
            {heading}
          </h2>
        </div>

        <div className="flex items-center gap-2">
          <button
            type="button"
            onClick={() => nudge(-1)}
            disabled={atStart}
            className={arrowClass}
          >
            <ChevronLeft aria-hidden className="h-5 w-5 rtl:rotate-180" />
            <span className="sr-only">Previous quotes</span>
          </button>
          <button type="button" onClick={() => nudge(1)} disabled={atEnd} className={arrowClass}>
            <ChevronRight aria-hidden className="h-5 w-5 rtl:rotate-180" />
            <span className="sr-only">More quotes</span>
          </button>
        </div>
      </div>

      {/*
        `relative` on the rail is load-bearing. Tailwind's `sr-only` is
        `position: absolute`, and an overflow box only clips an absolutely
        positioned descendant when it is also that descendant's containing
        block. Without it the per-quote "N out of 5" labels laid out at their
        static position — up to 1042px in on a 390px screen — escaped the rail
        and gave the whole PAGE a sideways scroll, pointing at nothing anyone
        could see. Same bug, same fix as the table on /compare.
      */}
      <ul
        ref={railRef}
        onScroll={syncEnds}
        tabIndex={0}
        role="group"
        aria-label="Customer quotes, scrollable"
        className="relative mt-10 flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4 [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background"
      >
        {testimonials.map((testimonial) => (
          <li
            key={testimonial.name}
            className="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]"
          >
            {testimonial.rating !== undefined && (
              <p className="flex items-center gap-0.5">
                <span className="sr-only">{testimonial.rating} out of 5</span>
                {Array.from({ length: 5 }, (_, i) => (
                  <Star
                    key={i}
                    aria-hidden
                    className={
                      i < (testimonial.rating ?? 0)
                        ? 'h-4 w-4 fill-primary text-primary'
                        : 'h-4 w-4 fill-muted text-muted'
                    }
                  />
                ))}
              </p>
            )}

            <figure className="mt-4 flex flex-1 flex-col">
              <blockquote className="flex-1">
                <p className="text-pretty leading-relaxed text-foreground">
                  &ldquo;{testimonial.quote}&rdquo;
                </p>
              </blockquote>

              <figcaption className="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
                <span
                  aria-hidden
                  className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary"
                >
                  {testimonial.name
                    .split(' ')
                    .slice(0, 2)
                    .map((word) => word[0] ?? '')
                    .join('')
                    .toUpperCase()}
                </span>
                <span className="text-sm leading-snug">
                  <span className="block font-semibold text-foreground">{testimonial.name}</span>
                  <span className="block text-muted-foreground">
                    {testimonial.role}, {testimonial.company}
                  </span>
                </span>
              </figcaption>
            </figure>
          </li>
        ))}
      </ul>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/testimonial-carousel.tsx and import it where you need the section:

import { TestimonialCarousel } from '@/components/testimonial-carousel'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
eyebrowstring'Customers'
headingstring'Twenty months of people telling us what they…
testimonialsCarouselTestimonial[]DEFAULT_TESTIMONIALS
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

testimonial-carousel.html
<!--
  Scroll-Snap Quote Rail — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section aria-labelledby="_R_0_" class="mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="flex flex-wrap items-end justify-between gap-6">
    <div class="max-w-2xl">
      <p class="text-sm font-medium uppercase tracking-wider text-primary">Customers</p>
      <h2 id="_R_0_" class="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl">Twenty months of people telling us what they actually thought</h2>
    </div>
    <div class="flex items-center gap-2">
      <button type="button" disabled="" class="flex h-10 w-10 items-center justify-center rounded-full border border-border/60 text-foreground transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left h-5 w-5 rtl:rotate-180" aria-hidden="true">
          <path d="m15 18-6-6 6-6"></path>
        </svg>
        <span class="sr-only">Previous quotes</span>
      </button>
      <button type="button" class="flex h-10 w-10 items-center justify-center rounded-full border border-border/60 text-foreground transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right h-5 w-5 rtl:rotate-180" aria-hidden="true">
          <path d="m9 18 6-6-6-6"></path>
        </svg>
        <span class="sr-only">More quotes</span>
      </button>
    </div>
  </div>
  <ul tabindex="0" role="group" aria-label="Customer quotes, scrollable" class="relative mt-10 flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4 [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background">
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">5 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“The migration took an afternoon, which I did not believe when sales said it. What actually saved us was that our old exports imported without a schema rewrite.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">DO</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Dara Okonkwo</span>
            <span class="block text-muted-foreground">Staff Engineer, Northwind Freight</span>
          </span>
        </figcaption>
      </figure>
    </li>
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">5 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“We bought it for one team and it spread to four without anyone asking me to approve a rollout. That has happened twice in nine years here.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">PR</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Priya Ramachandran</span>
            <span class="block text-muted-foreground">Director of Platform, Alcove Health</span>
          </span>
        </figcaption>
      </figure>
    </li>
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">5 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“Support answered a schema question at 2am our time with an actual query, not a docs link. I have kept the thread.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">TL</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Tomas Lindqvist</span>
            <span class="block text-muted-foreground">Data Lead, Ravel Studio</span>
          </span>
        </figcaption>
      </figure>
    </li>
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">4 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-muted text-muted" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“It is not the cheapest thing we evaluated. It is the only one where the trial ended and nobody asked to go back.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">AB</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Amara Belhadj</span>
            <span class="block text-muted-foreground">VP Engineering, Kestrel Systems</span>
          </span>
        </figcaption>
      </figure>
    </li>
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">5 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“Our audit went from a three-week scramble to a report we generate. I know that sounds like copy off your own website. It is what happened.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">RM</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Ruth Mbeki</span>
            <span class="block text-muted-foreground">Head of Compliance, Fieldstone Bank</span>
          </span>
        </figcaption>
      </figure>
    </li>
    <li class="flex w-[19rem] shrink-0 snap-start flex-col rounded-2xl border border-border/60 bg-card/80 p-6 sm:w-[22rem]">
      <p class="flex items-center gap-0.5">
        <span class="sr-only">4 out of 5</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-primary text-primary" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star h-4 w-4 fill-muted text-muted" aria-hidden="true">
          <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
        </svg>
      </p>
      <figure class="mt-4 flex flex-1 flex-col">
        <blockquote class="flex-1">
          <p class="text-pretty leading-relaxed text-foreground">“The onboarding was rough and we nearly gave up in week one. Two releases later the thing we were stuck on is a checkbox.”</p>
        </blockquote>
        <figcaption class="mt-6 flex items-center gap-3 border-t border-border/60 pt-4">
          <span aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">JW</span>
          <span class="text-sm leading-snug">
            <span class="block font-semibold text-foreground">Jonas Weber</span>
            <span class="block text-muted-foreground">Principal Architect, Meridian Logistics</span>
          </span>
        </figcaption>
      </figure>
    </li>
  </ul>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

5 more blocks in Testimonials

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Testimonials

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Testimonials blocks

View category
Open the full page for this block

Masonry Testimonial Wall

Quotes packed by height using CSS multi-column, with star ratings, initial avatars and no ragged bottom edge.

Testimonials140 lines1 dep
Open the full page for this block

Single-Quote Spotlight

One customer, quoted at length and backed by a stat row — the section for when a single believable story argues better than a wall of five-star fragments.

Testimonials110 lines1 dep
Open the full page for this block

Aggregate Rating Breakdown

A mean score over a stated sample with all five rating bands left visible, including the one-star row — proof that survives the objection every hand-picked quote wall invites.

Testimonials198 lines1 dep
Open the full page for this block

Video Testimonial Cards

Poster tiles linking out to recordings, each with a pull-quote that has to land for the majority who never press play, and a drawn placeholder until you supply a still.

Testimonials198 lines1 dep
Open the full page for this block

Customer Outcome Band

Testimonials reduced to the measured outcome and the conditions it happened under, for readers who skip the quotes.

Testimonials121 linesNo deps