shadimage
Shadcnspace Builder
blocktestimonial-02source: ok · refreshed

Testimonial 02 - Testimonial Slider Showcase

A clean testimonial slider with customer photo, detailed quote, navigation controls, and partner logos—perfect for highlighting success stories and building instant trust on landing pages.

npx shadcn add "https://builder.shadcnspace.com/r/testimonial-02.json"
free

Where it goes

testimonial

Visual style

minimal

Interaction

carousel

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Registry dependencies

badgecarousel

Files5 · source cached 10/2/2026, 1:26:41 AM

src/components/shadcn-space/blocks/testimonial-02/page.tsx
import Testimonial01 from "@/components/shadcn-space/blocks/testimonial-02";


export default function Page() {
    return <Testimonial01 />;
}
src/components/shadcn-space/blocks/testimonial-02/testimonial.tsx
"use client";

import { useRef } from "react";
import { Badge } from "@/components/ui/badge";
import { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } from "@/components/ui/carousel";
import { motion, useInView } from "motion/react";

export interface Testimonial {
    quote: string;
    author: string;
    role: string;
    image: string;
}

export interface Testimonial01Props {
    badge?: string;
    title?: string;
    testimonials?: Testimonial[];
}

const defaultTestimonials: Testimonial[] = [
    {
        quote: "Shadcn Space replaced messy UI kits and half-baked templates. Today our dashboards look premium, scale beautifully, and our team focuses on real features instead of design debt.",
        author: "Walter Dutcher",
        role: "CEO",
        image: "https://images.shadcnspace.com/assets/profiles/testimonial-user.png",
    },
    {
        quote: "Shadcn Space replaced messy UI kits and half-baked templates. Today our dashboards look premium, scale beautifully, and our team focuses on real features instead of design debt.",
        author: "Walter Dutcher",
        role: "CEO",
        image: "https://images.shadcnspace.com/assets/profiles/testimonial-user-2.png",
    },
];

export default function Testimonial01({
    testimonials = defaultTestimonials,
}: Testimonial01Props) {
    const sectionRef = useRef<HTMLElement>(null);
    const isInView = useInView(sectionRef, { once: true, amount: 0.1 });

    return (
        <section ref={sectionRef}>
            <div className="max-w-7xl mx-auto sm:px-16 px-4 pt-12">
                <div className="">
                    <motion.div
                        initial={{ opacity: 0, y: -40 }}
                        animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: -40 }}
                        transition={{ duration: 1, delay: 0.2, ease: "easeInOut" }}
                        className="flex flex-col gap-3"
                    >
                        <Badge className="text-sm h-auto py-1 px-3 border-0 w-fit">
                            Testimonials
                        </Badge>
                        <h2 className="sm:text-5xl text-xl leading-none font-medium tracking">
                            Success Stories
                        </h2>
                    </motion.div>
                    <motion.div
                        initial={{ opacity: 0, y: 40 }}
                        animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 40 }}
                        transition={{ duration: 1, delay: 0.2, ease: "easeInOut" }}
                        className="pt-12 pb-8"
                    >
                        <Carousel>
                            <CarouselContent>
                                {testimonials.map((testimonial, index) => (
                                    <CarouselItem key={index}>
                                        <div className="grid grid-cols-12 gap-6 items-center">
                                            <div className="lg:col-span-8 col-span-12 flex sm:flex-row flex-col sm:gap-10 gap-6 lg:pe-12">
                                                <div className="shrink-0 flex items-start">
                                                    <img
                                                        src="https://images.shadcnspace.com/assets/svgs/icon-quote.svg"
                                                        alt="muted quote"
                                                        className="dark:hidden"
                                                    />
                                                    <img
                                                        src="https://images.shadcnspace.com/assets/svgs/icon-quote-white.svg"
                                                        alt="muted quote"
                                                        className="hidden dark:block"
                                                    />
                                                </div>
                                                <div className="flex flex-col gap-12">
                                                    <p className="sm:text-4xl text-xl text-muted-foreground">
                                                        {testimonial.quote}
                                                    </p>
                                                    <div>
                                                        <p className="text-base font-medium ">
                                                            {testimonial.author}
                                                        </p>
                                                        <p className="text-sm text-muted-foreground">
                                                            {testimonial.role}
                                                        </p>
                                                    </div>
                                                </div>
                                            </div>
                                            <div className="md:col-span-4 col-span-12">
                                                <div className="rounded-xl overflow-hidden">
                                                    <img
                                                        src={testimonial.image}
                                                        alt={testimonial.author}
                                                        width={500}
                                                        height={500}
                                                        className="w-full h-full object-cover"
                                                    />
                                                </div>
                                            </div>
                                        </div>
                                    </CarouselItem>
                                ))}
                            </CarouselContent>
                            <CarouselPrevious className={"-top-20 left-auto right-12"} />
                            <CarouselNext className={"-top-20 right-0"} />
                        </Carousel>
                    </motion.div>
                </div>
            </div>
        </section>
    );
}
src/components/shadcn-space/blocks/testimonial-02/index.tsx
import BrandSlider, { BrandList } from "@/components/shadcn-space/blocks/testimonial-02/brand-slider";
import Testimonial01Inner, { Testimonial } from "@/components/shadcn-space/blocks/testimonial-02/testimonial";

const brandList: BrandList[] = [
    {
        image: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-1.svg",
        lightimg: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-white-1.svg",
        name: "Brand 1",
    },
    {
        image: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-2.svg",
        lightimg: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-white-2.svg",
        name: "Brand 2",
    },
    {
        image: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-3.svg",
        lightimg: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-white-3.svg",
        name: "Brand 3",
    },
    {
        image: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-4.svg",
        lightimg: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-white-4.svg",
        name: "Brand 4",
    },
    {
        image: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-5.svg",
        lightimg: "https://images.shadcnspace.com/assets/brand-logo/logoipsum-muted-white-5.svg",
        name: "Brand 5",
    },
];

const defaultTestimonials: Testimonial[] = [
    {
        quote: "Shadcn Space replaced messy UI kits and half-baked templates. Today our dashboards look premium, scale beautifully, and our team focuses on real features instead of design debt.",
        author: "Walter Dutcher",
        role: "CEO",
        image: "https://images.shadcnspace.com/assets/profiles/testimonial-user.png",
    },
    {
        quote: "Shadcn Space replaced messy UI kits and half-baked templates. Today our dashboards look premium, scale beautifully, and our team focuses on real features instead of design debt.",
        author: "Errica Mcdowell",
        role: "Marketing Head",
        image: "https://images.shadcnspace.com/assets/profiles/testimonial-user-2.png",
    },
];

export default function Testimonial01() {
    return (
        <main>
            <div className="min-h-screen flex flex-col justify-center">
                <Testimonial01Inner testimonials={defaultTestimonials} />
                <BrandSlider brandList={brandList} />
            </div>
        </main>
    );
}
src/components/shadcn-space/blocks/testimonial-02/brand-slider.tsx
"use client";

import { useRef } from "react";
import { Marquee } from "@/components/shadcn-space/animations/marquee";
import { motion, useInView } from "motion/react";

export interface BrandList {
  image: string;
  name: string;
  lightimg: string;
}

function BrandSlider({ brandList }: { brandList: BrandList[] }) {
  const sectionRef = useRef<HTMLElement>(null);
  const isInView = useInView(sectionRef, { once: true, amount: 0.1 });

  return (
    <section ref={sectionRef}>
      <div className="py-6 md:py-10">
        <div className="max-w-7xl mx-auto sm:px-16 px-4">
          <motion.div
            initial={{ opacity: 0, y: 40 }}
            animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 40 }}
            transition={{ duration: 1, delay: 0.2, ease: "easeInOut" }}
            className="flex flex-col gap-6 pt-12 border-t border-border"
          >
            <p className="text-base font-normal sm:px-2 px-10 text-muted-foreground text-center">
              More than 12,000 businesses delight their customers with Shadcn Space
            </p>
            {brandList && brandList.length > 0 && (
              <div className="py-4">
                <Marquee pauseOnHover className="[--duration:20s] p-0">
                  {brandList.map((brand, index) => (
                    <div key={index}>
                      <img
                        src={brand.image}
                        alt={brand.name}
                        className="w-36 h-8 mr-6 lg:mr-20 dark:hidden"
                      />
                      <img
                        src={brand.lightimg}
                        alt={brand.name}
                        className="hidden dark:block w-36 h-8 mr-12 lg:mr-20"
                      />
                    </div>
                  ))}
                </Marquee>
              </div>
            )}
          </motion.div>
        </div>
      </div>
    </section>
  );
}

export default BrandSlider;
src/components/shadcn-space/animations/marquee.tsx
"use client";

import { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";

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;
}

export function Marquee({
  className,
  reverse = false,
  pauseOnHover = false,
  children,
  vertical = false,
  repeat = 4,
  ...props
}: MarqueeProps) {
  return (
    <>
      <style>{`
        @keyframes marquee {
          from { transform: translateX(0); }
          to { transform: translateX(calc(-100% - var(--gap))); }
        }
        @keyframes marquee-vertical {
          from { transform: translateY(0); }
          to { transform: translateY(calc(-100% - var(--gap))); }
        }
        .animate-marquee {
          animation: marquee var(--duration) linear infinite;
        }
        .animate-marquee-vertical {
          animation: marquee-vertical var(--duration) linear infinite;
        }
      `}</style>
      <div
        {...props}
        className={cn(
          "group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]",
          {
            "flex-row": !vertical,
            "flex-col": vertical,
          },
          className
        )}
      >
        {Array(repeat)
          .fill(0)
          .map((_, i) => (
            <div
              key={i}
              className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
                "animate-marquee": !vertical,
                "animate-marquee-vertical": vertical,
                "group-hover:[animation-play-state:paused]": pauseOnHover,
                "[animation-direction:reverse]": reverse,
              })}
            >
              {children}
            </div>
          ))}
      </div>
    </>
  );
}

More from Shadcnspace Builder

Browse all components in this library →