Skip to main content
Back to Blog
Tutorials
3 min read
November 28, 2024

How to Build a Testimonial Carousel with React and Tailwind CSS

Create a responsive testimonial carousel with autoplay, navigation controls, and smooth animations using React and Tailwind CSS.

Ryel Banfield

Founder & Lead Developer

Social proof drives conversions. A testimonial carousel lets you showcase multiple reviews in limited space. Here is how to build one from scratch.

Step 1: Define Testimonial Data

type Testimonial = {
  id: number;
  name: string;
  role: string;
  company: string;
  content: string;
  avatar: string;
  rating: number;
};

const testimonials: Testimonial[] = [
  {
    id: 1,
    name: "Sarah Mitchell",
    role: "Marketing Director",
    company: "GrowthCo",
    content:
      "They delivered our new website two weeks ahead of schedule. Traffic increased 180% in the first quarter. The ROI has been incredible.",
    avatar: "/images/avatars/sarah.jpg",
    rating: 5,
  },
  {
    id: 2,
    name: "James Park",
    role: "Founder",
    company: "TechStart",
    content:
      "Our custom software solution handles 10x the traffic of our previous system. The team understood our technical requirements perfectly.",
    avatar: "/images/avatars/james.jpg",
    rating: 5,
  },
  {
    id: 3,
    name: "Maria Rodriguez",
    role: "Operations Manager",
    company: "ServicePro",
    content:
      "The mobile app they built has a 4.8 star rating. Customer engagement doubled. They are now our go-to development partner.",
    avatar: "/images/avatars/maria.jpg",
    rating: 5,
  },
];

Step 2: Build the Star Rating Component

function StarRating({ rating }: { rating: number }) {
  return (
    <div className="flex gap-0.5" aria-label={`${rating} out of 5 stars`}>
      {Array.from({ length: 5 }).map((_, i) => (
        <svg
          key={i}
          className={`h-5 w-5 ${i < rating ? "text-yellow-400" : "text-gray-300"}`}
          fill="currentColor"
          viewBox="0 0 20 20"
        >
          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
        </svg>
      ))}
    </div>
  );
}

Step 3: Build the Carousel

"use client";

import { useState, useEffect, useCallback } from "react";

export function TestimonialCarousel() {
  const [current, setCurrent] = useState(0);
  const [isPaused, setIsPaused] = useState(false);

  const next = useCallback(() => {
    setCurrent((prev) => (prev + 1) % testimonials.length);
  }, []);

  const prev = useCallback(() => {
    setCurrent((prev) => (prev - 1 + testimonials.length) % testimonials.length);
  }, []);

  // Autoplay
  useEffect(() => {
    if (isPaused) return;
    const interval = setInterval(next, 5000);
    return () => clearInterval(interval);
  }, [isPaused, next]);

  // Keyboard navigation
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === "ArrowLeft") prev();
      if (e.key === "ArrowRight") next();
    }
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [next, prev]);

  const testimonial = testimonials[current];

  return (
    <section
      className="py-20"
      onMouseEnter={() => setIsPaused(true)}
      onMouseLeave={() => setIsPaused(false)}
      aria-roledescription="carousel"
      aria-label="Customer testimonials"
    >
      <div className="mx-auto max-w-3xl px-6 text-center">
        <h2 className="text-3xl font-bold text-gray-900 dark:text-white">
          What Our Clients Say
        </h2>

        <div
          className="mt-12"
          role="group"
          aria-roledescription="slide"
          aria-label={`${current + 1} of ${testimonials.length}`}
        >
          <StarRating rating={testimonial.rating} />

          <blockquote className="mt-6">
            <p className="text-xl leading-relaxed text-gray-700 dark:text-gray-300">
              &ldquo;{testimonial.content}&rdquo;
            </p>
          </blockquote>

          <div className="mt-6 flex items-center justify-center gap-4">
            <img
              src={testimonial.avatar}
              alt={testimonial.name}
              className="h-12 w-12 rounded-full object-cover"
            />
            <div className="text-left">
              <p className="font-semibold text-gray-900 dark:text-white">
                {testimonial.name}
              </p>
              <p className="text-sm text-gray-500">
                {testimonial.role}, {testimonial.company}
              </p>
            </div>
          </div>
        </div>

        {/* Navigation */}
        <div className="mt-10 flex items-center justify-center gap-4">
          <button
            onClick={prev}
            className="rounded-full border p-2 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-800"
            aria-label="Previous testimonial"
          >
            <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
            </svg>
          </button>

          {/* Dots */}
          <div className="flex gap-2">
            {testimonials.map((_, i) => (
              <button
                key={i}
                onClick={() => setCurrent(i)}
                className={`h-2.5 w-2.5 rounded-full transition-colors ${
                  i === current ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-600"
                }`}
                aria-label={`Go to testimonial ${i + 1}`}
              />
            ))}
          </div>

          <button
            onClick={next}
            className="rounded-full border p-2 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-800"
            aria-label="Next testimonial"
          >
            <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
            </svg>
          </button>
        </div>
      </div>
    </section>
  );
}

Step 4: Add Slide Transition Animation

For smoother transitions, add CSS:

/* globals.css */
.testimonial-enter {
  opacity: 0;
  transform: translateX(20px);
}

.testimonial-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 300ms ease, transform 300ms ease;
}

Or use Framer Motion:

import { AnimatePresence, motion } from "framer-motion";

<AnimatePresence mode="wait">
  <motion.div
    key={current}
    initial={{ opacity: 0, x: 20 }}
    animate={{ opacity: 1, x: 0 }}
    exit={{ opacity: 0, x: -20 }}
    transition={{ duration: 0.3 }}
  >
    {/* testimonial content */}
  </motion.div>
</AnimatePresence>

Step 5: Multi-Card Layout (Desktop)

Show three testimonials at once on larger screens:

function TestimonialGrid() {
  return (
    <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
      {testimonials.map((t) => (
        <div
          key={t.id}
          className="rounded-xl border p-6 dark:border-gray-700"
        >
          <StarRating rating={t.rating} />
          <p className="mt-4 text-gray-600 dark:text-gray-300">
            &ldquo;{t.content}&rdquo;
          </p>
          <div className="mt-4 flex items-center gap-3">
            <img
              src={t.avatar}
              alt={t.name}
              className="h-10 w-10 rounded-full"
            />
            <div>
              <p className="text-sm font-medium">{t.name}</p>
              <p className="text-xs text-gray-500">{t.company}</p>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

Accessibility Checklist

  • Include aria-roledescription="carousel" on the container
  • Use aria-label on each slide group
  • Pause autoplay on hover and focus
  • Support keyboard navigation with arrow keys
  • Ensure sufficient color contrast for text and controls

Need Social Proof for Your Website?

We design conversion-focused websites with testimonial sections that build trust. Contact us to get started.

ReactcarouselTailwind CSStestimonialstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles