Open in ![Sarah Chen](/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1535713875002-d1d0cf377fde%3Fq%3D80%26w%3D3560%26auto%3Dformat%26fit%3Dcrop%26ixlib%3Drb-4.0.3%26ixid%3DM3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%253D%253D&w=1080&q=75)
![Michael Rodriguez](/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1438761681033-6461ffad8d80%3Fq%3D80%26w%3D3540%26auto%3Dformat%26fit%3Dcrop%26ixlib%3Drb-4.0.3%26ixid%3DM3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%253D%253D&w=1080&q=75)
![Emily Watson](/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1623582854588-d60de57fa33f%3Fq%3D80%26w%3D3540%26auto%3Dformat%26fit%3Dcrop%26ixlib%3Drb-4.0.3%26ixid%3DM3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%253D%253D&w=1080&q=75)
![James Kim](/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1636041293178-808a6762ab39%3Fq%3D80%26w%3D3464%26auto%3Dformat%26fit%3Dcrop%26ixlib%3Drb-4.0.3%26ixid%3DM3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%253D%253D&w=1080&q=75)
![Lisa Thompson](/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1624561172888-ac93c696e10c%3Fq%3D80%26w%3D2592%26auto%3Dformat%26fit%3Dcrop%26ixlib%3Drb-4.0.3%26ixid%3DM3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%253D%253D&w=1080&q=75)
Sarah Chen
Product Manager at TechFlow
The attention to detail and innovative features have completely transformed our workflow. This is exactly what we've been looking for.
Installation
Install dependencies
npm i motion clsx tailwind-merge @tabler/icons-react
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/animated-testimonials.tsx
"use client";
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import { useEffect, useState } from "react";
type Testimonial = {
quote: string;
name: string;
designation: string;
src: string;
};
export const AnimatedTestimonials = ({
testimonials,
autoplay = false,
}: {
testimonials: Testimonial[];
autoplay?: boolean;
}) => {
const [active, setActive] = useState(0);
const handleNext = () => {
setActive((prev) => (prev + 1) % testimonials.length);
};
const handlePrev = () => {
setActive((prev) => (prev - 1 + testimonials.length) % testimonials.length);
};
const isActive = (index: number) => {
return index === active;
};
useEffect(() => {
if (autoplay) {
const interval = setInterval(handleNext, 5000);
return () => clearInterval(interval);
}
}, [autoplay]);
const randomRotateY = () => {
return Math.floor(Math.random() * 21) - 10;
};
return (
<div className="max-w-sm md:max-w-4xl mx-auto antialiased font-sans px-4 md:px-8 lg:px-12 py-20">
<div className="relative grid grid-cols-1 md:grid-cols-2 gap-20">
<div>
<div className="relative h-80 w-full">
<AnimatePresence>
{testimonials.map((testimonial, index) => (
<motion.div
key={testimonial.src}
initial={{
opacity: 0,
scale: 0.9,
z: -100,
rotate: randomRotateY(),
}}
animate={{
opacity: isActive(index) ? 1 : 0.7,
scale: isActive(index) ? 1 : 0.95,
z: isActive(index) ? 0 : -100,
rotate: isActive(index) ? 0 : randomRotateY(),
zIndex: isActive(index)
? 999
: testimonials.length + 2 - index,
y: isActive(index) ? [0, -80, 0] : 0,
}}
exit={{
opacity: 0,
scale: 0.9,
z: 100,
rotate: randomRotateY(),
}}
transition={{
duration: 0.4,
ease: "easeInOut",
}}
className="absolute inset-0 origin-bottom"
>
<Image
src={testimonial.src}
alt={testimonial.name}
width={500}
height={500}
draggable={false}
className="h-full w-full rounded-3xl object-cover object-center"
/>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
<div className="flex justify-between flex-col py-4">
<motion.div
key={active}
initial={{
y: 20,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
exit={{
y: -20,
opacity: 0,
}}
transition={{
duration: 0.2,
ease: "easeInOut",
}}
>
<h3 className="text-2xl font-bold dark:text-white text-black">
{testimonials[active].name}
</h3>
<p className="text-sm text-gray-500 dark:text-neutral-500">
{testimonials[active].designation}
</p>
<motion.p className="text-lg text-gray-500 mt-8 dark:text-neutral-300">
{testimonials[active].quote.split(" ").map((word, index) => (
<motion.span
key={index}
initial={{
filter: "blur(10px)",
opacity: 0,
y: 5,
}}
animate={{
filter: "blur(0px)",
opacity: 1,
y: 0,
}}
transition={{
duration: 0.2,
ease: "easeInOut",
delay: 0.02 * index,
}}
className="inline-block"
>
{word}
</motion.span>
))}
</motion.p>
</motion.div>
<div className="flex gap-4 pt-12 md:pt-0">
<button
onClick={handlePrev}
className="h-7 w-7 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button"
>
<IconArrowLeft className="h-5 w-5 text-black dark:text-neutral-400 group-hover/button:rotate-12 transition-transform duration-300" />
</button>
<button
onClick={handleNext}
className="h-7 w-7 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button"
>
<IconArrowRight className="h-5 w-5 text-black dark:text-neutral-400 group-hover/button:-rotate-12 transition-transform duration-300" />
</button>
</div>
</div>
</div>
</div>
);
};
Props
Prop Name | Type | Required | Default | Description |
---|---|---|---|---|
testimonials | Array<{ quote: string; name: string; designation: string; src: string }> | Yes | - | Array of testimonial objects containing the quote, author name, designation, and image source URL |
autoplay | boolean | No | false | Whether to automatically cycle through testimonials every 5 seconds |
Testimonial Object Shape
Each testimonial in the testimonials
array should have the following properties:
Property | Type | Description |
---|---|---|
quote | string | The testimonial text |
name | string | The name of the person giving the testimonial |
designation | string | The title or role of the person |
src | string | The URL of the person's image |
Get beautiful, hand-crafted templates and components with Aceternity UI Pro
Professional, beautiful and elegant templates for your business. Get the best component packs and templates with Aceternity UI Pro.
Manu was quick to respond, very professional, and delivered a website within a week. Very good job. Looking forward to collaborating again
Asriel Han
Founder, CTO at Advex AI