Testimonial Carousel

Auto-advancing testimonial carousel with prev/next arrows, dot indicators, and pause-on-hover.

Preview

Source

tsx
"use client";

import { useEffect, useState } from "react";
import { ChevronLeft, ChevronRight, Star } from "lucide-react";
import { testimonials } from "@/lib/placeholders";

export default function TestimonialCarousel() {
  const [active, setActive] = useState(0);
  const [paused, setPaused] = useState(false);

  const prev = () => setActive((a) => (a - 1 + testimonials.length) % testimonials.length);
  const next = () => setActive((a) => (a + 1) % testimonials.length);

  useEffect(() => {
    if (paused) return;
    const id = setInterval(() => {
      setActive((a) => (a + 1) % testimonials.length);
    }, 4000);
    return () => clearInterval(id);
  }, [paused]);

  const t = testimonials[active];

  return (
    <section
      className="py-20 px-4 sm:px-6 bg-background"
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
    >
      <div className="max-w-3xl mx-auto">
        {/* Header */}
        <div className="text-center mb-12">
          <span className="inline-block text-xs font-semibold uppercase tracking-widest text-primary bg-primary/10 border border-primary/20 rounded-full px-4 py-1.5 mb-4">
            What clients say
          </span>
          <h2 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-foreground">
            Real results, real people
          </h2>
        </div>

        {/* Card */}
        <div className="relative bg-card border border-border rounded-3xl p-8 sm:p-12 shadow-lg text-center">
          {/* Decorative quote mark */}
          <div className="absolute top-6 left-8 text-7xl font-serif text-primary/10 select-none leading-none">
            "
          </div>

          {/* Stars */}
          <div className="flex justify-center gap-1 mb-6">
            {Array.from({ length: 5 }).map((_, i) => (
              <Star key={i} className="w-5 h-5 fill-amber-400 text-amber-400" />
            ))}
          </div>

          {/* Quote */}
          <blockquote className="text-xl sm:text-2xl font-medium text-foreground leading-relaxed mb-8">
            "{t.text}"
          </blockquote>

          {/* Avatar + name */}
          <div className="flex flex-col items-center gap-3">
            <img
              src={t.avatar}
              alt={t.name}
              className="w-14 h-14 rounded-full object-cover ring-2 ring-primary/20"
            />
            <div>
              <p className="font-bold text-foreground">{t.name}</p>
              <p className="text-sm text-muted-foreground">{t.role}</p>
            </div>
          </div>

          {/* Prev / Next */}
          <div className="flex justify-between items-center mt-8">
            <button
              onClick={prev}
              className="w-10 h-10 rounded-full border border-border bg-background hover:bg-muted transition-colors flex items-center justify-center text-muted-foreground hover:text-foreground"
              aria-label="Previous testimonial"
            >
              <ChevronLeft className="w-5 h-5" />
            </button>

            {/* Dots */}
            <div className="flex gap-2">
              {testimonials.map((_, i) => (
                <button
                  key={i}
                  onClick={() => setActive(i)}
                  className={`h-2 rounded-full transition-all duration-300 ${
                    i === active
                      ? "w-6 bg-primary"
                      : "w-2 bg-border hover:bg-muted-foreground"
                  }`}
                  aria-label={`Go to testimonial ${i + 1}`}
                />
              ))}
            </div>

            <button
              onClick={next}
              className="w-10 h-10 rounded-full border border-border bg-background hover:bg-muted transition-colors flex items-center justify-center text-muted-foreground hover:text-foreground"
              aria-label="Next testimonial"
            >
              <ChevronRight className="w-5 h-5" />
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add carousel

Where to use it

Great for hero sections, standalone proof sections, or anywhere you want a single bold testimonial in focus. In Astro: import TestimonialCarousel from '../components/innovations/testimonials/carousel'; <TestimonialCarousel client:load /> In Next.js: import TestimonialCarousel from '@/components/innovations/testimonials/carousel'; // Place after the hero or between feature sections The carousel auto-advances every 4 seconds and pauses on hover. Edit testimonials in src/lib/placeholders.ts or pass a custom array.