Masonry Testimonial Wall
Quotes packed by height using CSS multi-column, with star ratings, initial avatars and no ragged bottom edge.
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.
components/testimonial-carousel.tsxnpx hoverlab add testimonial-carousel
Or over MCP, from your editor's agent — no account needed.
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.
Start a page with this section — add more, order them, and leave with the page source.
Customers
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'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">
“{testimonial.quote}”
</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>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/testimonial-carousel.tsx and import it where you need the section:
import { TestimonialCarousel } from '@/components/testimonial-carousel'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.
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.
| Prop | Type | Default |
|---|---|---|
eyebrow | string | 'Customers' |
heading | string | 'Twenty months of people telling us what they… |
testimonials | CarouselTestimonial[] | DEFAULT_TESTIMONIALS |
className | string | '' |
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.
<!--
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>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
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.
Want the whole screen instead of this one section? Open a page and copy it entire.
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 TestimonialsCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Quotes packed by height using CSS multi-column, with star ratings, initial avatars and no ragged bottom edge.
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.
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.
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.
Testimonials reduced to the measured outcome and the conditions it happened under, for readers who skip the quotes.