Back to library

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

Ava Kim

Product Designer

We cut our build times in half the first week. It just works.

Daniel Ortiz

Daniel Ortiz

CTO, Framestack

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

This is the first tool that actually keeps up with how fast our team iterates.

Ava Kim

Ava Kim

Product Designer

We cut our build times in half the first week. It just works.

Daniel Ortiz

Daniel Ortiz

CTO, Framestack

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

Replaced three subscriptions with this one tool. Easy decision.

Lena Fischer

Lena Fischer

Founder, Loopwell

Rare to find something this polished. The details are incredible.

Chris Yamamoto

Chris Yamamoto

Staff Engineer

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

Replaced three subscriptions with this one tool. Easy decision.

Lena Fischer

Lena Fischer

Founder, Loopwell

Rare to find something this polished. The details are incredible.

Chris Yamamoto

Chris Yamamoto

Staff Engineer

We cut our build times in half the first week. It just works.

Daniel Ortiz

Daniel Ortiz

CTO, Framestack

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

We cut our build times in half the first week. It just works.

Daniel Ortiz

Daniel Ortiz

CTO, Framestack

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

Replaced three subscriptions with this one tool. Easy decision.

Lena Fischer

Lena Fischer

Founder, Loopwell

Shipped my whole MVP over a weekend. The defaults are chef's kiss.

Maya Patel

Maya Patel

Indie Hacker

The API design is so clean that onboarding new devs takes minutes.

Tom Becker

Tom Becker

Engineering Lead

Replaced three subscriptions with this one tool. Easy decision.

Lena Fischer

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

components/ui/testimonials-3d.tsx
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>
  );
}