sections
3D Testimonials
Vertically scrolling testimonial marquees tilted in 3D perspective. Pause on hover, reverse alternating columns.
Inspired by sean0205 · View source
Preview
This is the first tool that actually keeps up with how fast our team iterates.
Ava Kim
Product Designer
We cut our build times in half the first week. It just works.
Daniel Ortiz
CTO, Framestack
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
This is the first tool that actually keeps up with how fast our team iterates.
Ava Kim
Product Designer
We cut our build times in half the first week. It just works.
Daniel Ortiz
CTO, Framestack
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
Replaced three subscriptions with this one tool. Easy decision.
Lena Fischer
Founder, Loopwell
Rare to find something this polished. The details are incredible.
Chris Yamamoto
Staff Engineer
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
Replaced three subscriptions with this one tool. Easy decision.
Lena Fischer
Founder, Loopwell
Rare to find something this polished. The details are incredible.
Chris Yamamoto
Staff Engineer
We cut our build times in half the first week. It just works.
Daniel Ortiz
CTO, Framestack
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
We cut our build times in half the first week. It just works.
Daniel Ortiz
CTO, Framestack
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
Replaced three subscriptions with this one tool. Easy decision.
Lena Fischer
Founder, Loopwell
Shipped my whole MVP over a weekend. The defaults are chef's kiss.
Maya Patel
Indie Hacker
The API design is so clean that onboarding new devs takes minutes.
Tom Becker
Engineering Lead
Replaced three subscriptions with this one tool. Easy decision.
Lena Fischer
Founder, Loopwell
Copy for AI
Copy for Lovable is the fastest way to move this component into another Lovable project — it copies the full prompt with dependencies, files, and a usage example. Copy all files gives you the raw source if you want to paste it manually.
Installation
npx shadcn@latest add "https://21st.dev/r/sean0205/3d-testimonails"Paste into a terminal — or paste into a Lovable chat and I'll run it for you.
Source files
import React, { ComponentPropsWithoutRef, useRef } from "react";
import { cn } from "@/lib/utils";
import "./testimonials-3d.css";
interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
/**
* Optional CSS class name to apply custom styles
*/
className?: string;
/**
* Whether to reverse the animation direction
* @default false
*/
reverse?: boolean;
/**
* Whether to pause the animation on hover
* @default false
*/
pauseOnHover?: boolean;
/**
* Content to be displayed in the marquee
*/
children: React.ReactNode;
/**
* Whether to animate vertically instead of horizontally
* @default false
*/
vertical?: boolean;
/**
* Number of times to repeat the content
* @default 4
*/
repeat?: number;
/**
* ARIA label for accessibility
*/
ariaLabel?: string;
/**
* ARIA live region politeness
*/
ariaLive?: "off" | "polite" | "assertive";
/**
* ARIA role
*/
ariaRole?: string;
}
export function Marquee({
className,
reverse = false,
pauseOnHover = false,
children,
vertical = false,
repeat = 4,
ariaLabel,
ariaLive = "off",
ariaRole = "marquee",
...props
}: MarqueeProps) {
const marqueeRef = useRef<HTMLDivElement>(null);
return (
<div
{...props}
ref={marqueeRef}
data-slot="marquee"
className={cn(
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
{
"flex-row": !vertical,
"flex-col": vertical,
},
className,
)}
aria-label={ariaLabel}
aria-live={ariaLive}
role={ariaRole}
tabIndex={0}
>
{React.useMemo(
() => (
<>
{Array.from({ length: repeat }, (_, i) => (
<div
key={i}
className={cn(
!vertical ? "flex-row [gap:var(--gap)]" : "flex-col [gap:var(--gap)]",
"flex shrink-0 justify-around",
!vertical && "animate-marquee flex-row",
vertical && "animate-marquee-vertical flex-col",
pauseOnHover && "group-hover:[animation-play-state:paused]",
reverse && "[animation-direction:reverse]",
)}
>
{children}
</div>
))}
</>
),
[repeat, children, vertical, pauseOnHover, reverse],
)}
</div>
);
}