Wide quote cards with tilt turned off so the text stays level, plus page dots.
@keyframeui
registry/demos/arc-scroll-testimonials-demo.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { ArcScroll } from "@/components/ui/arc-scroll";
const testimonials = [
{
quote: "We replaced three libraries with it and our bundle got smaller. The motion feels hand-tuned.",
name: "Maya Okafor",
role: "Design Engineer, Parcel",
avatar: "from-amber-300 to-rose-400",
},
{
quote: "Our landing page finally feels alive. Copy, paste, adjust two props, ship.",
name: "Daniel Reyes",
role: "Founder, Fieldnote",
avatar: "from-emerald-300 to-teal-500",
},
{
quote: "Reduced motion support out of the box saved us a full accessibility pass.",
name: "Priya Shah",
role: "Frontend Lead, Halcyon",
avatar: "from-sky-300 to-indigo-500",
},
{
quote: "It reads like code I'd write myself, which is the highest compliment I have.",
name: "Tom Lindqvist",
role: "Staff Engineer, Orbit",
avatar: "from-violet-300 to-fuchsia-500",
},
{
quote: "Our designers stopped asking for After Effects exports. That alone was worth it.",
name: "Aiko Tanaka",
role: "Product Designer, Lumen",
avatar: "from-lime-300 to-emerald-500",
},
];
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("");
}
export function ArcScrollTestimonialsDemo() {
const scrollRef = React.useRef<HTMLDivElement>(null);
const [active, setActive] = React.useState(0);
return (
<div className="relative w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950">
<div ref={scrollRef} className="relative h-[var(--demo-height,32rem)] overflow-y-auto overscroll-contain">
<ArcScroll
containerRef={scrollRef}
stageClassName="h-[var(--demo-height,32rem)]"
spacing={30}
tilt={false}
scrollPerCard={300}
onActiveChange={setActive}
overlay={
<>
<p className="absolute inset-x-0 top-6 text-center font-mono text-xs uppercase tracking-widest text-zinc-500">
Loved by teams who ship
</p>
<div className="absolute inset-x-0 bottom-8 flex justify-center gap-2">
{testimonials.map((item, index) => (
<span
key={item.name}
className={cn(
"h-1.5 rounded-full transition-all duration-300",
index === active
? "w-5 bg-zinc-900 dark:bg-zinc-50"
: "w-1.5 bg-zinc-300 dark:bg-zinc-700",
)}
/>
))}
</div>
</>
}
>
{testimonials.map((item) => (
<figure
key={item.name}
className="flex h-52 w-64 flex-col justify-between rounded-2xl bg-white p-5 shadow-xl ring-1 shadow-zinc-900/10 ring-zinc-200 sm:w-72 dark:bg-zinc-900 dark:shadow-black/50 dark:ring-zinc-800"
>
<blockquote className="text-[15px] leading-relaxed text-zinc-700 dark:text-zinc-300">
“{item.quote}”
</blockquote>
<figcaption className="flex items-center gap-3">
<span
aria-hidden
className={`flex size-9 items-center justify-center rounded-full bg-linear-to-br text-xs font-semibold text-white ${item.avatar}`}
>
{initials(item.name)}
</span>
<span className="text-sm">
<span className="block font-medium text-zinc-900 dark:text-zinc-50">
{item.name}
</span>
<span className="block text-zinc-500">{item.role}</span>
</span>
</figcaption>
</figure>
))}
</ArcScroll>
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/arc-scroll#arc-scroll-testimonials-demo























