Testimonials 2
Testimonials 2 is a React and Tailwind CSS layout component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
layout
What it is
Testimonials 2 is a React and Tailwind CSS layout component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i lucide-react motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Testimonials 2
"use client";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { getAvatarUrl, getTestimonials } from "@/lib/smoothui-data";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useCallback, useEffect, useState } from "react";
const testimonials = getTestimonials(7);
export function TestimonialsGrid() {
const shouldReduceMotion = useReducedMotion();
const [active, setActive] = useState(0);
const [autoplay] = useState(false);
const handleNext = useCallback(() => {
setActive((prev) => (prev + 1) % testimonials.length);
}, []);
const handlePrev = () => {
setActive((prev) => (prev - 1 + testimonials.length) % testimonials.length);
};
const isActive = (index: number) => index === active;
useEffect(() => {
if (autoplay) {
const interval = setInterval(handleNext, 5000);
return () => clearInterval(interval);
}
}, [autoplay, handleNext]);
return (
<section>
<div className="min-h-auto bg-muted py-24">
<div className="container mx-auto w-full max-w-6xl px-6">
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
className="mb-12"
initial={
shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.6, ease: [0.22, 1, 0.36, 1] }
}
>
{/* Layout: Title on left, Testimonial on right */}
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2 lg:gap-12">
{/* Left side - Title and description */}
<div className="flex flex-col justify-center">
<h2 className="mb-4 font-semibold text-4xl text-foreground">
What Developers Say
</h2>
<p className="text-balance text-foreground/70 text-lg">
Join thousands of developers who are building faster, more
beautiful UIs with SmoothUI. See what they're saying
about their experience.
</p>
</div>
{/* Right side - Testimonial card */}
<div className="relative flex min-h-fit flex-col items-end">
{/* Navigation Arrows - Above the card */}
<div className="mb-4 flex justify-center gap-2">
<motion.button
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }
}
aria-label="Previous testimonial"
className="group/button flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl"
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, x: -20 }
}
onClick={handlePrev}
transition={
shouldReduceMotion
? { duration: 0 }
: { delay: 0.4, duration: 0.3 }
}
type="button"
whileHover={shouldReduceMotion ? {} : { scale: 1.1 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
>
<ChevronLeft className="h-5 w-5 text-foreground transition-transform duration-300 group-hover/button:-rotate-12" />
</motion.button>
<motion.button
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }
}
aria-label="Next testimonial"
className="group/button flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl"
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, x: 20 }
}
onClick={handleNext}
transition={
shouldReduceMotion
? { duration: 0 }
: { delay: 0.4, duration: 0.3 }
}
type="button"
whileHover={shouldReduceMotion ? {} : { scale: 1.1 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
>
<ChevronRight className="h-5 w-5 text-foreground transition-transform duration-300 group-hover/button:rotate-12" />
</motion.button>
</div>
<div className="relative h-full w-full max-w-md">
<AnimatePresence>
{testimonials.map((testimonial, index) => (
<motion.div
animate={
shouldReduceMotion
? { opacity: isActive(index) ? 1 : 0 }
: {
opacity: isActive(index) ? 1 : 0,
scale: isActive(index) ? 1 : 0.95,
y: isActive(index) ? 0 : 30,
}
}
className={`absolute inset-0 min-h-fit ${isActive(index) ? "z-10" : "z-0"}`}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: {
opacity: 0,
scale: 0.9,
y: -30,
}
}
initial={
shouldReduceMotion
? { opacity: 0 }
: {
opacity: 0,
scale: 0.9,
y: 30,
}
}
key={testimonial.name}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 0.4,
ease: "easeInOut",
}
}
>
<div className="rounded-2xl border bg-background px-6 py-6 shadow-lg transition-all duration-200">
<motion.p
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, y: 0 }
}
className="mb-6 text-foreground text-lg"
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, y: 10 }
}
key={active}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.3 }
}
>
{(testimonial.content || "")
.split(" ")
.map((word, wordIndex) => (
<motion.span
animate={
shouldReduceMotion
? { opacity: 1 }
: {
filter: "blur(0px)",
opacity: 1,
y: 0,
}
}
className="inline-block"
initial={
shouldReduceMotion
? { opacity: 1 }
: {
filter: "blur(4px)",
opacity: 0,
y: 5,
}
}
key={`${testimonial.name}-word-${wordIndex}`}
transition={
shouldReduceMotion
? { duration: 0 }
: {
delay: wordIndex * 0.02,
duration: 0.2,
ease: "easeInOut",
}
}
>
{word}
</motion.span>
))}
</motion.p>
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, y: 0 }
}
className="flex items-center gap-3"
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, y: 10 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { delay: 0.2, duration: 0.3 }
}
>
<Avatar className="size-8 border border-transparent shadow ring-1 ring-foreground/10">
<AvatarImage
alt={testimonial.name}
src={getAvatarUrl(testimonial.avatar, 64)}
/>
<AvatarFallback>
{testimonial.name.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-semibold text-foreground">
{testimonial.name}
</div>
<span className="text-muted-foreground text-sm">
{testimonial.role}
</span>
</div>
</motion.div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
export default TestimonialsGrid;